@k2b/cloud 0.38.0 → 0.40.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/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { keyboardPlatform } from "@k2b/ui";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { defineApp } from "../_internal/define-app";
|
|
4
|
+
import { person } from "../_internal/define-app.test-fixture";
|
|
5
|
+
import type { AppAppearance, CloudRuntime } from "../contracts/app";
|
|
6
|
+
import type { User } from "../contracts/shared";
|
|
7
|
+
import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
|
|
8
|
+
import { appAppearanceStyle } from "./app-appearance";
|
|
9
|
+
import Layout from "./Layout";
|
|
10
|
+
import { LayoutHelpPage, type LayoutHelpPageProps, LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelp";
|
|
11
|
+
import { helpArticlePartsServer } from "./layout-help-article.server";
|
|
12
|
+
import PanelLayout from "./PanelLayout";
|
|
13
|
+
import { readPanelContext } from "./panel-context";
|
|
14
|
+
|
|
15
|
+
export const documents: HelpDocumentManifest[] = [
|
|
16
|
+
{ id: "spaces-share", title: "Share a Space", kind: "task", icon: "ti ti-share" },
|
|
17
|
+
{ id: "spaces-start", title: "Start with Spaces", kind: "start", icon: "ti ti-rocket" },
|
|
18
|
+
].map((topic) => ({
|
|
19
|
+
...topic,
|
|
20
|
+
kind: topic.id === "spaces-share" ? "task" : "start",
|
|
21
|
+
order: 1,
|
|
22
|
+
url: `/api/help/${topic.id}`,
|
|
23
|
+
searchUrl: "/api/help/search",
|
|
24
|
+
}));
|
|
25
|
+
export const helpManifest = { documents, pageBase: "/help/apps/spaces", baseLocale: "en", locale: "en", manifestHash: "fixture" };
|
|
26
|
+
export const fixtureDocument: HelpDocumentPayload = {
|
|
27
|
+
id: "spaces-share",
|
|
28
|
+
title: "Share a Space",
|
|
29
|
+
locale: "en",
|
|
30
|
+
markdown: "Share",
|
|
31
|
+
kind: "task",
|
|
32
|
+
needs: "manage",
|
|
33
|
+
related: [],
|
|
34
|
+
// A lead, the facts chips, and a section: everything the article head reads from the article.
|
|
35
|
+
html: '<p>Share a Space so other people can see and edit its tasks.</p><h2 id="share">Share</h2><div class="help-steps"><ol><li>Open Space settings.</li><li>Add a person.</li></ol></div>',
|
|
36
|
+
};
|
|
37
|
+
const initialArticle = { payload: fixtureDocument, parts: helpArticlePartsServer(fixtureDocument.html) };
|
|
38
|
+
const appearance: AppAppearance = { accent: "#16a34a", background: { from: "#16a34a", to: "#0284c7" } };
|
|
39
|
+
const apps: CloudRuntime["apps"] = [
|
|
40
|
+
{
|
|
41
|
+
id: "spaces",
|
|
42
|
+
name: "Spaces",
|
|
43
|
+
icon: "ti ti-box",
|
|
44
|
+
description: "Help fixture",
|
|
45
|
+
routes: ["/app/spaces"],
|
|
46
|
+
nav: { href: "/app/spaces", section: "primary" },
|
|
47
|
+
appearance,
|
|
48
|
+
help: helpManifest,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "core",
|
|
52
|
+
name: "Core",
|
|
53
|
+
icon: "ti ti-cloud",
|
|
54
|
+
description: "Core fixture",
|
|
55
|
+
routes: ["/"],
|
|
56
|
+
panel: { href: "/panel", label: "Help", icon: "ti ti-help-circle" },
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
const app = defineApp({
|
|
60
|
+
id: "help-fixture",
|
|
61
|
+
name: "Help fixture",
|
|
62
|
+
description: "Help fixture",
|
|
63
|
+
icon: "ti ti-help-circle",
|
|
64
|
+
baseUrl: "http://fixture:3000",
|
|
65
|
+
routes: ["/"],
|
|
66
|
+
});
|
|
67
|
+
type Environment = { Variables: { runtime: CloudRuntime; user: User } };
|
|
68
|
+
const PropsSnapshot = (props: { value: LayoutHelpPanelProps | LayoutHelpPageProps }) => (
|
|
69
|
+
<script id="help-fixture-props" type="application/json" innerHTML={JSON.stringify(props.value).replace(/</g, "\\u003c")} />
|
|
70
|
+
);
|
|
71
|
+
export const server = new Hono<Environment>()
|
|
72
|
+
.use(async (c, next) => {
|
|
73
|
+
c.set("runtime", { apps });
|
|
74
|
+
c.set("user", person);
|
|
75
|
+
await next();
|
|
76
|
+
})
|
|
77
|
+
.get(
|
|
78
|
+
"/app/spaces",
|
|
79
|
+
...app.ssr<Environment>((c) => () => (
|
|
80
|
+
<Layout c={c} title="Spaces" help={["spaces-share"]}>
|
|
81
|
+
<p>App content</p>
|
|
82
|
+
</Layout>
|
|
83
|
+
)),
|
|
84
|
+
)
|
|
85
|
+
.get(
|
|
86
|
+
"/panel",
|
|
87
|
+
...app.ssr<Environment>((c) => {
|
|
88
|
+
const context = readPanelContext(c);
|
|
89
|
+
const props: LayoutHelpPanelProps = {
|
|
90
|
+
documents,
|
|
91
|
+
app: { id: "spaces", name: "Spaces", icon: "ti ti-box" },
|
|
92
|
+
appearance: appAppearanceStyle(appearance),
|
|
93
|
+
view: context?.app === "spaces" ? context.help?.filter((id) => documents.some((topic) => topic.id === id)) : [],
|
|
94
|
+
platform: keyboardPlatform(c.req.header("User-Agent") ?? ""),
|
|
95
|
+
pageBase: helpManifest.pageBase,
|
|
96
|
+
initialArticle: c.req.query("topic") === "spaces-share" ? initialArticle : undefined,
|
|
97
|
+
initialTopic: c.req.query("topic") === "spaces-share" ? "spaces-share" : undefined,
|
|
98
|
+
};
|
|
99
|
+
return () => (
|
|
100
|
+
<PanelLayout c={c} title="Help">
|
|
101
|
+
<div id="help-fixture-root" class="flex h-full min-h-0 flex-1 flex-col">
|
|
102
|
+
<LayoutHelpPanel {...props} />
|
|
103
|
+
</div>
|
|
104
|
+
<PropsSnapshot value={props} />
|
|
105
|
+
</PanelLayout>
|
|
106
|
+
);
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
.get(
|
|
110
|
+
"/help/apps/spaces/spaces-share",
|
|
111
|
+
...app.ssr<Environment>((c) => () => (
|
|
112
|
+
<Layout c={c} title="Help">
|
|
113
|
+
<div id="help-fixture-root">
|
|
114
|
+
<LayoutHelpPage
|
|
115
|
+
documents={documents}
|
|
116
|
+
initialTopic="spaces-share"
|
|
117
|
+
initialArticle={initialArticle}
|
|
118
|
+
pageBase={helpManifest.pageBase}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<PropsSnapshot value={{ documents, initialTopic: "spaces-share", initialArticle, pageBase: helpManifest.pageBase }} />
|
|
122
|
+
</Layout>
|
|
123
|
+
)),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Capture during parsing and immediately after mounting; coordinates inside the provider are relative to its viewport.
|
|
128
|
+
* The article head keeps its position, width and height; the contents column keeps its position and width.
|
|
129
|
+
*/
|
|
130
|
+
export function helpPanelGeometry() {
|
|
131
|
+
return ["[data-help-view]", "[data-cloud-app-area]", ".help-article-head", ".help-article-aside"].map((selector, index) => {
|
|
132
|
+
const rect = document.querySelector(selector)?.getBoundingClientRect();
|
|
133
|
+
if (!rect) return null;
|
|
134
|
+
return index < 3 ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : { x: rect.x, y: rect.y, width: rect.width };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { IconButton, Tooltip, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, onCleanup, onMount } from "solid-js";
|
|
3
|
+
import { isServer } from "solid-js/web";
|
|
4
|
+
import { closePanel, openPanel } from "../browser/panel";
|
|
2
5
|
import { importOnDemand } from "../browser/reload";
|
|
3
6
|
import { createBusyAction } from "./busy-action";
|
|
4
7
|
import { platformMessages } from "./platform-messages";
|
|
@@ -6,6 +9,8 @@ import { platformMessages } from "./platform-messages";
|
|
|
6
9
|
type LayoutHelpTriggerProps = {
|
|
7
10
|
variant: "header" | "rail";
|
|
8
11
|
class?: string;
|
|
12
|
+
panel?: boolean;
|
|
13
|
+
helpOpen?: boolean;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
/** Opens end-user help from either the desktop rail or the compact header. */
|
|
@@ -13,17 +18,35 @@ export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
|
|
|
13
18
|
const locale = useLocale();
|
|
14
19
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
15
20
|
const action = createBusyAction();
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
// The server knows the cookie; the browser also knows the mode: a phone starts with the sheet closed, whatever the
|
|
22
|
+
// cookie says, and the lazy controller may have changed the state before this island mounts.
|
|
23
|
+
const readOpen = () => {
|
|
24
|
+
const canvas = document.querySelector(".cloud-app-canvas");
|
|
25
|
+
const attribute = window.matchMedia("(min-width: 768px)").matches ? "data-panel" : "data-panel-sheet";
|
|
26
|
+
return canvas?.getAttribute(attribute) === "open" && document.getElementById("cloud-panel")?.dataset.provider === "core";
|
|
27
|
+
};
|
|
28
|
+
const [helpOpen, setHelpOpen] = createSignal(isServer ? !!props.helpOpen : !!props.panel && readOpen());
|
|
29
|
+
onMount(() => {
|
|
30
|
+
const changed = (event: WindowEventMap["cloud.panel.state"]) => setHelpOpen(event.detail.open && event.detail.provider === "core");
|
|
31
|
+
window.addEventListener("cloud.panel.state", changed);
|
|
32
|
+
onCleanup(() => window.removeEventListener("cloud.panel.state", changed));
|
|
33
|
+
});
|
|
34
|
+
const openHelp = () => {
|
|
35
|
+
if (helpOpen()) return closePanel();
|
|
36
|
+
if (openPanel("core")) return;
|
|
37
|
+
return action.run(
|
|
18
38
|
() => importOnDemand(() => import("./LayoutHelp")),
|
|
19
39
|
(help) => help.openLayoutHelpDialog(),
|
|
20
40
|
);
|
|
41
|
+
};
|
|
21
42
|
|
|
22
43
|
if (props.variant === "rail")
|
|
23
44
|
return (
|
|
24
45
|
<Tooltip.Trigger
|
|
25
46
|
type="button"
|
|
26
47
|
class={`rail-item text-blue-500 hover:bg-blue-500/10 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/15 dark:hover:text-blue-300 ${props.class ?? ""}`}
|
|
48
|
+
aria-controls={props.panel ? "cloud-panel" : undefined}
|
|
49
|
+
aria-expanded={props.panel ? helpOpen() : undefined}
|
|
27
50
|
onClick={openHelp}
|
|
28
51
|
aria-busy={action.busy() ? "true" : undefined}
|
|
29
52
|
aria-label={t().openHelp}
|
|
@@ -38,6 +61,8 @@ export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
|
|
|
38
61
|
return (
|
|
39
62
|
<IconButton
|
|
40
63
|
class={props.class}
|
|
64
|
+
aria-controls={props.panel ? "cloud-panel" : undefined}
|
|
65
|
+
aria-expanded={props.panel ? helpOpen() : undefined}
|
|
41
66
|
onClick={openHelp}
|
|
42
67
|
aria-busy={action.busy() ? "true" : undefined}
|
|
43
68
|
label={t().openHelp}
|
package/src/ssr/LayoutRail.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import type { RailApp } from "./rail-navigation";
|
|
|
5
5
|
export type LayoutAppLink = RailApp;
|
|
6
6
|
|
|
7
7
|
/** The rail frame and Home stay server markup; only the navigation is interactive. */
|
|
8
|
-
export default function LayoutRail(props: { currentPath: string }) {
|
|
8
|
+
export default function LayoutRail(props: { currentPath: string; panelOpen?: boolean }) {
|
|
9
9
|
const locale = useLocale();
|
|
10
10
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
11
11
|
return (
|
|
@@ -15,7 +15,7 @@ export default function LayoutRail(props: { currentPath: string }) {
|
|
|
15
15
|
<img src="/branding/logo" alt="Logo" class="h-5 w-5" />
|
|
16
16
|
</a>
|
|
17
17
|
</div>
|
|
18
|
-
<CloudShell slot="rail" currentPath={props.currentPath} />
|
|
18
|
+
<CloudShell slot="rail" currentPath={props.currentPath} panelOpen={props.panelOpen} />
|
|
19
19
|
</aside>
|
|
20
20
|
);
|
|
21
21
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TabBar, useLocale } from "@k2b/ui";
|
|
2
|
+
import { platformMessages } from "./platform-messages";
|
|
3
|
+
import { phoneBarItems, projectRailNavigation } from "./rail-navigation";
|
|
4
|
+
import { useShellData } from "./shell-data";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The signed-in phone's bottom bar: Home and the person's first apps in their rail order, floating on the app
|
|
8
|
+
* gradient above the content card. It is server markup, so it stands in its final place before any island mounts and
|
|
9
|
+
* a link focused early keeps its focus. The rail's island takes over only when the bar's content changes in the
|
|
10
|
+
* browser: after the person saves their rail preferences, or when the URL changes in the page. The
|
|
11
|
+
* place of the app's primary action beside it stays free. From 768 px the rail takes over and the bar is hidden.
|
|
12
|
+
*/
|
|
13
|
+
export default function LayoutTabBar(props: { currentPath: string }) {
|
|
14
|
+
const data = useShellData();
|
|
15
|
+
const locale = useLocale();
|
|
16
|
+
if (!data?.rail) return null;
|
|
17
|
+
const t = platformMessages.resolve([locale()]).t;
|
|
18
|
+
const items = phoneBarItems(projectRailNavigation(data.apps, data.rail, locale()), props.currentPath, {
|
|
19
|
+
label: t.home,
|
|
20
|
+
path: data.home ?? "/",
|
|
21
|
+
});
|
|
22
|
+
return (
|
|
23
|
+
<div class="cloud-tab-bar">
|
|
24
|
+
<div class="cloud-tab-bar__row">
|
|
25
|
+
<TabBar variant="floating" label={t.apps} items={items} />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -48,8 +48,8 @@ function MobileNavigation(props: {
|
|
|
48
48
|
value={tab}
|
|
49
49
|
onValueChange={setTab}
|
|
50
50
|
options={[
|
|
51
|
-
{ value: "app", label: workspace()?.label || t().appMenu },
|
|
52
|
-
{ value: "apps", label: t().allApps },
|
|
51
|
+
{ value: "app", label: <span class="block k2b-truncate-fade">{workspace()?.label || t().appMenu}</span> },
|
|
52
|
+
{ value: "apps", label: <span class="block k2b-truncate-fade">{t().allApps}</span> },
|
|
53
53
|
]}
|
|
54
54
|
/>
|
|
55
55
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LocaleProvider } from "@k2b/ui";
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
import { canonicalRoute } from "../_internal/app-path";
|
|
4
|
+
import { getLocale } from "../server/locale";
|
|
5
|
+
import { resolveRuntimeRoute } from "./app-navigation";
|
|
6
|
+
import type { MinimalLayoutContext } from "./layout-context";
|
|
7
|
+
import PanelRuntime from "./PanelRuntime.island";
|
|
8
|
+
import { readPanelContext } from "./panel-context";
|
|
9
|
+
import { getRuntimeContext } from "./runtime";
|
|
10
|
+
|
|
11
|
+
export type PanelLayoutProps = {
|
|
12
|
+
c: MinimalLayoutContext & { header(name: string, value: string): void };
|
|
13
|
+
title: string;
|
|
14
|
+
children: JSX.Element;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** A provider page embedded in the shell's same-origin panel. */
|
|
18
|
+
export default function PanelLayout(props: PanelLayoutProps) {
|
|
19
|
+
props.c.header("Referrer-Policy", "same-origin");
|
|
20
|
+
const context = readPanelContext(props.c);
|
|
21
|
+
const runtime = getRuntimeContext(props.c);
|
|
22
|
+
// The provider owns this path by the canonical prefixes its panel declaration was checked against.
|
|
23
|
+
const apps = runtime.apps.map((entry) => ({ ...entry, routes: entry.routes.map(canonicalRoute) }));
|
|
24
|
+
const app = resolveRuntimeRoute(apps, new URL(props.c.req.raw.url).pathname)?.app;
|
|
25
|
+
props.c.get("page").title = props.title;
|
|
26
|
+
return (
|
|
27
|
+
<LocaleProvider locale={getLocale(props.c)}>
|
|
28
|
+
<div class="cloud-panel-page" data-cloud-panel-page="" data-panel-provider={app?.id}>
|
|
29
|
+
<script id="cloud-panel-context" type="application/json" innerHTML={JSON.stringify(context).replace(/</g, "\\u003c")} />
|
|
30
|
+
<PanelRuntime provider={app?.id} scope={app?.panel ? new URL(app.panel.href, props.c.req.raw.url).pathname : undefined} />
|
|
31
|
+
{props.children}
|
|
32
|
+
</div>
|
|
33
|
+
</LocaleProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { installAppWorkspaceController, installInputModeController, keyboardPlatform } from "@k2b/ui";
|
|
2
|
+
import { onCleanup, onMount } from "solid-js";
|
|
3
|
+
import { installPanelContextReceiver } from "../browser/panel-context-receiver";
|
|
4
|
+
import { syncTimezoneCookie } from "./timezone-cookie";
|
|
5
|
+
|
|
6
|
+
export default function PanelRuntime(props: { provider?: string; scope?: string }) {
|
|
7
|
+
onMount(() => {
|
|
8
|
+
syncTimezoneCookie(true);
|
|
9
|
+
const disposeInput = installInputModeController();
|
|
10
|
+
// The SSR body hands workspace controller ownership to the page runtime, as Layout's ShellRuntime does. A panel
|
|
11
|
+
// page keeps no workspace layout cookie: the app's own layout must not follow the reader into a narrow panel.
|
|
12
|
+
const disposeWorkspace = installAppWorkspaceController();
|
|
13
|
+
const deferred = new Set<ReturnType<typeof setTimeout>>();
|
|
14
|
+
const embedded = window.parent !== window;
|
|
15
|
+
let disposeTheme = () => {};
|
|
16
|
+
let disposeContext = () => {};
|
|
17
|
+
let resumeTheme = () => {};
|
|
18
|
+
// A page kept in the back/forward cache keeps following the host; only a removed iframe lets go.
|
|
19
|
+
const pageHide = (event: PageTransitionEvent) => {
|
|
20
|
+
if (!event.persisted) disposeTheme();
|
|
21
|
+
};
|
|
22
|
+
const pageShow = (event: PageTransitionEvent) => {
|
|
23
|
+
if (event.persisted) resumeTheme();
|
|
24
|
+
};
|
|
25
|
+
const close = () => window.parent.postMessage({ type: "cloud.panel.close" }, location.origin);
|
|
26
|
+
const click = (event: MouseEvent) => {
|
|
27
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
28
|
+
const anchor = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>("a[href]") : null;
|
|
29
|
+
if (!anchor || (anchor.target && anchor.target !== "_self") || anchor.hasAttribute("download")) return;
|
|
30
|
+
const url = new URL(anchor.getAttribute("href")!, document.baseURI);
|
|
31
|
+
if (url.origin !== location.origin) return;
|
|
32
|
+
const scope = props.scope?.replace(/\/$/, "");
|
|
33
|
+
if (scope !== undefined && (url.pathname === scope || url.pathname.startsWith(`${scope}/`))) return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
window.parent.location.assign(url.href);
|
|
36
|
+
};
|
|
37
|
+
const key = (event: KeyboardEvent) => {
|
|
38
|
+
if (event.isComposing || event.defaultPrevented || event.altKey || event.shiftKey) return;
|
|
39
|
+
const apple = keyboardPlatform() === "apple";
|
|
40
|
+
if (event.key.toLowerCase() === "j" && event.metaKey === apple && event.ctrlKey === !apple) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
if (!event.repeat) close();
|
|
43
|
+
} else if (event.key.toLowerCase() === "k" && event.metaKey === apple && event.ctrlKey === !apple) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
if (!event.repeat) window.parent.postMessage({ type: "cloud.panel.search", v: 1 }, location.origin);
|
|
46
|
+
} else if (event.key === "Escape" && !event.metaKey && !event.ctrlKey) {
|
|
47
|
+
// An open modal dialog takes Escape through its cancel event, without preventing this keydown.
|
|
48
|
+
if (document.querySelector("dialog[open], [aria-modal='true']")) return;
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
deferred.delete(timer);
|
|
51
|
+
if (!event.defaultPrevented) close();
|
|
52
|
+
}, 0);
|
|
53
|
+
deferred.add(timer);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (embedded) {
|
|
57
|
+
disposeContext = installPanelContextReceiver();
|
|
58
|
+
try {
|
|
59
|
+
const parent = window.parent;
|
|
60
|
+
const parentRoot = parent.document.documentElement;
|
|
61
|
+
const syncTheme = () => {
|
|
62
|
+
for (const theme of ["light", "dark"]) {
|
|
63
|
+
document.documentElement.classList.toggle(theme, parentRoot.classList.contains(theme));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
parent.addEventListener("cloud:theme-preference", syncTheme);
|
|
67
|
+
disposeTheme = () => parent.removeEventListener("cloud:theme-preference", syncTheme);
|
|
68
|
+
resumeTheme = syncTheme;
|
|
69
|
+
syncTheme();
|
|
70
|
+
} catch {
|
|
71
|
+
// A cross-origin parent cannot share its document or theme events.
|
|
72
|
+
}
|
|
73
|
+
window.addEventListener("pagehide", pageHide);
|
|
74
|
+
window.addEventListener("pageshow", pageShow);
|
|
75
|
+
// The shell reads this mark when its controller arrives after the message.
|
|
76
|
+
if (props.provider) document.documentElement.dataset.cloudPanelReady = props.provider;
|
|
77
|
+
window.parent.postMessage({ type: "cloud.panel.ready", v: 1, provider: props.provider }, location.origin);
|
|
78
|
+
window.addEventListener("keydown", key);
|
|
79
|
+
window.addEventListener("click", click);
|
|
80
|
+
}
|
|
81
|
+
onCleanup(() => {
|
|
82
|
+
disposeContext();
|
|
83
|
+
disposeInput();
|
|
84
|
+
disposeWorkspace();
|
|
85
|
+
disposeTheme();
|
|
86
|
+
window.removeEventListener("pagehide", pageHide);
|
|
87
|
+
window.removeEventListener("pageshow", pageShow);
|
|
88
|
+
window.removeEventListener("keydown", key);
|
|
89
|
+
window.removeEventListener("click", click);
|
|
90
|
+
for (const timer of deferred) clearTimeout(timer);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Button, IconButton, type KeyboardPlatform, NoticeCard, Placeholder, ResizeSeparator, useLocale } from "@k2b/ui";
|
|
2
|
+
import { PANEL_MAX_WIDTH_REM, PANEL_MIN_WIDTH_REM, type PanelCookieState } from "../_internal/panel-cookie";
|
|
3
|
+
import { withPanelContext } from "../contracts/panel-context-plain";
|
|
4
|
+
import { platformMessages } from "./platform-messages";
|
|
5
|
+
import type { ShellData } from "./shell-data";
|
|
6
|
+
|
|
7
|
+
/** Server markup only. The shell loads its controller on demand. */
|
|
8
|
+
export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]>; state: PanelCookieState; platform?: KeyboardPlatform }) {
|
|
9
|
+
const locale = useLocale();
|
|
10
|
+
const t = platformMessages.resolve([locale()]).t;
|
|
11
|
+
const provider = props.panel.providers.find((provider) => provider.id === props.panel.provider) ?? props.panel.providers[0];
|
|
12
|
+
if (!provider) return null;
|
|
13
|
+
const answer = props.panel.consent?.provider === provider.id ? props.panel.consent.allowed : null;
|
|
14
|
+
const ask = props.state.open && provider.consent && answer === null;
|
|
15
|
+
const allowed = !provider.consent || answer === true;
|
|
16
|
+
const hasConsent = props.panel.providers.some((provider) => provider.consent);
|
|
17
|
+
const tabs = props.panel.providers.length > 1;
|
|
18
|
+
return (
|
|
19
|
+
<aside
|
|
20
|
+
class="cloud-panel"
|
|
21
|
+
id="cloud-panel"
|
|
22
|
+
data-cloud-panel=""
|
|
23
|
+
aria-label={t.panel}
|
|
24
|
+
data-state={props.state.open ? "loading" : "closed"}
|
|
25
|
+
data-provider={provider.id}
|
|
26
|
+
data-app-name={provider.appName}
|
|
27
|
+
data-provider-consent={String(provider.consent)}
|
|
28
|
+
data-context-allowed={String(allowed)}
|
|
29
|
+
data-consent={ask ? "ask" : undefined}
|
|
30
|
+
>
|
|
31
|
+
<ResizeSeparator
|
|
32
|
+
class="cloud-panel__resize"
|
|
33
|
+
label={t.resizePanel}
|
|
34
|
+
orientation="vertical"
|
|
35
|
+
controls="cloud-panel"
|
|
36
|
+
min={PANEL_MIN_WIDTH_REM}
|
|
37
|
+
max={PANEL_MAX_WIDTH_REM}
|
|
38
|
+
value={props.state.widthRem ?? 26}
|
|
39
|
+
grow="start"
|
|
40
|
+
onResize={() => {}}
|
|
41
|
+
/>
|
|
42
|
+
<div class="cloud-panel__card">
|
|
43
|
+
<button type="button" class="cloud-panel__grabber" data-cloud-panel-grow="" aria-label={t.enlargePanel}>
|
|
44
|
+
<span aria-hidden="true" />
|
|
45
|
+
</button>
|
|
46
|
+
<div class="cloud-panel__head">
|
|
47
|
+
{tabs ? (
|
|
48
|
+
<div class="cloud-panel__tabs" role="tablist" aria-label={t.panels}>
|
|
49
|
+
{props.panel.providers.map((item) => (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
role="tab"
|
|
53
|
+
class="cloud-panel__tab"
|
|
54
|
+
data-cloud-panel-provider={item.id}
|
|
55
|
+
aria-selected={item.id === provider.id}
|
|
56
|
+
aria-controls="cloud-panel-body"
|
|
57
|
+
tabindex={item.id === provider.id ? 0 : -1}
|
|
58
|
+
>
|
|
59
|
+
<i class={item.icon} aria-hidden="true" />
|
|
60
|
+
<span class="cloud-panel__tab-label k2b-truncate-fade">{item.label}</span>
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
) : (
|
|
65
|
+
<h2 class="cloud-panel__title">
|
|
66
|
+
<i class={provider.icon} aria-hidden="true" />
|
|
67
|
+
<span>{provider.label}</span>
|
|
68
|
+
</h2>
|
|
69
|
+
)}
|
|
70
|
+
{/* The controller mounts the ⋯ menu over this button; the place stays reserved for Core providers. */}
|
|
71
|
+
{hasConsent && (
|
|
72
|
+
<span
|
|
73
|
+
class="cloud-panel__menu"
|
|
74
|
+
data-cloud-panel-menu=""
|
|
75
|
+
style={{ visibility: provider.consent ? "visible" : "hidden" }}
|
|
76
|
+
inert={!provider.consent}
|
|
77
|
+
>
|
|
78
|
+
<IconButton label={t.panelMenu} title={t.panelMenu} aria-haspopup="menu" aria-expanded="false">
|
|
79
|
+
<i class="ti ti-dots" aria-hidden="true" />
|
|
80
|
+
</IconButton>
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
<IconButton
|
|
84
|
+
label={t.closePanel}
|
|
85
|
+
title={`${t.closePanel} (${props.platform === "apple" ? "⌘J" : "Ctrl+J"})`}
|
|
86
|
+
data-cloud-panel-close=""
|
|
87
|
+
>
|
|
88
|
+
<i class="ti ti-x" aria-hidden="true" />
|
|
89
|
+
</IconButton>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="cloud-panel__body" id="cloud-panel-body" role={tabs ? "tabpanel" : undefined}>
|
|
92
|
+
{hasConsent && (
|
|
93
|
+
// The shell asks, not the provider: the question sits above the provider page, which loads without context.
|
|
94
|
+
<NoticeCard
|
|
95
|
+
class="cloud-panel__consent"
|
|
96
|
+
tone="neutral"
|
|
97
|
+
data-cloud-panel-consent=""
|
|
98
|
+
role="region"
|
|
99
|
+
tabindex="-1"
|
|
100
|
+
aria-labelledby="cloud-panel-consent-title"
|
|
101
|
+
title={
|
|
102
|
+
<span id="cloud-panel-consent-title" data-cloud-panel-consent-title="">
|
|
103
|
+
{t.panelConsentTitle({ app: provider.appName })}
|
|
104
|
+
</span>
|
|
105
|
+
}
|
|
106
|
+
detail={<span data-cloud-panel-consent-body="">{t.panelConsentBody({ app: provider.appName })}</span>}
|
|
107
|
+
>
|
|
108
|
+
<div class="cloud-panel__consent-actions">
|
|
109
|
+
<Button size="sm" variant="primary" data-cloud-panel-consent-answer="allow">
|
|
110
|
+
{t.panelConsentAllow}
|
|
111
|
+
</Button>
|
|
112
|
+
<Button size="sm" variant="secondary" data-cloud-panel-consent-answer="deny">
|
|
113
|
+
{t.panelConsentDeny}
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
{/* A failed save reports in this reserved line, so the question never grows. */}
|
|
117
|
+
<p class="cloud-panel__consent-error" data-cloud-panel-consent-error="" role="status" />
|
|
118
|
+
</NoticeCard>
|
|
119
|
+
)}
|
|
120
|
+
<div class="cloud-panel__skeleton" aria-hidden="true">
|
|
121
|
+
<span />
|
|
122
|
+
<span />
|
|
123
|
+
<span />
|
|
124
|
+
</div>
|
|
125
|
+
{/* Lazy: below 768 px the open cookie leaves the slot hidden, and a hidden iframe must not load its provider. */}
|
|
126
|
+
{props.state.open && (
|
|
127
|
+
<iframe
|
|
128
|
+
class="cloud-panel__frame"
|
|
129
|
+
src={withPanelContext(provider.href, allowed ? props.panel.context : null)}
|
|
130
|
+
title={provider.label}
|
|
131
|
+
loading="lazy"
|
|
132
|
+
data-cloud-panel-frame={provider.id}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
<div class="cloud-panel__fallback">
|
|
136
|
+
<Placeholder
|
|
137
|
+
state="error"
|
|
138
|
+
variant="panel"
|
|
139
|
+
title={t.panelLoadFailed}
|
|
140
|
+
description={t.panelNotResponding({ label: provider.label })}
|
|
141
|
+
action={<Button data-cloud-panel-retry="">{t.retry}</Button>}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="cloud-panel__shield" aria-hidden="true" />
|
|
145
|
+
<div class="cloud-panel__closing" aria-hidden="true">
|
|
146
|
+
<span>
|
|
147
|
+
<i class="ti ti-x" />
|
|
148
|
+
{t.releaseClosesPanel}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div role="status" class="k2b-sr-only" data-cloud-panel-status="">
|
|
154
|
+
{props.state.open ? t.panelLoading({ label: provider.label }) : ""}
|
|
155
|
+
</div>
|
|
156
|
+
</aside>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { IconButton, Tooltip, useLocale } from "@k2b/ui";
|
|
2
|
+
import { isServer } from "solid-js/web";
|
|
3
|
+
import type { PanelRequest } from "../browser/panel";
|
|
4
|
+
import { platformMessages } from "./platform-messages";
|
|
5
|
+
import { useShellData } from "./shell-data";
|
|
6
|
+
|
|
7
|
+
export default function PanelTrigger(props: { variant: "rail" | "header"; open?: boolean }) {
|
|
8
|
+
const locale = useLocale();
|
|
9
|
+
const data = useShellData();
|
|
10
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
11
|
+
const title = () => t().panelShortcut({ mod: data?.platform === "apple" ? "⌘" : "Ctrl+" });
|
|
12
|
+
// The lazy controller may have changed state before this island mounts.
|
|
13
|
+
const open = () =>
|
|
14
|
+
isServer
|
|
15
|
+
? !!props.open
|
|
16
|
+
: document
|
|
17
|
+
.querySelector(".cloud-app-canvas")
|
|
18
|
+
?.getAttribute(window.matchMedia("(min-width: 768px)").matches ? "data-panel" : "data-panel-sheet") === "open";
|
|
19
|
+
const toggle = () => window.dispatchEvent(new CustomEvent<PanelRequest>("cloud.panel.request", { detail: { action: "toggle" } }));
|
|
20
|
+
if (props.variant === "rail")
|
|
21
|
+
return (
|
|
22
|
+
<Tooltip.Trigger
|
|
23
|
+
type="button"
|
|
24
|
+
class="rail-item"
|
|
25
|
+
aria-label={t().openPanel}
|
|
26
|
+
aria-controls="cloud-panel"
|
|
27
|
+
aria-expanded={open()}
|
|
28
|
+
data-cloud-panel-toggle=""
|
|
29
|
+
content={title()}
|
|
30
|
+
placement="right"
|
|
31
|
+
delay={0}
|
|
32
|
+
onClick={toggle}
|
|
33
|
+
>
|
|
34
|
+
<i class="ti ti-layout-sidebar-right text-xl" aria-hidden="true" />
|
|
35
|
+
</Tooltip.Trigger>
|
|
36
|
+
);
|
|
37
|
+
return (
|
|
38
|
+
<IconButton
|
|
39
|
+
label={t().openPanel}
|
|
40
|
+
title={title()}
|
|
41
|
+
aria-controls="cloud-panel"
|
|
42
|
+
aria-expanded={open()}
|
|
43
|
+
data-cloud-panel-toggle=""
|
|
44
|
+
onClick={toggle}
|
|
45
|
+
>
|
|
46
|
+
<i class="ti ti-layout-sidebar-right text-base" aria-hidden="true" />
|
|
47
|
+
</IconButton>
|
|
48
|
+
);
|
|
49
|
+
}
|