@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
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
installAppWorkspaceController,
|
|
5
5
|
installInputModeController,
|
|
6
6
|
installPageTitleController,
|
|
7
|
+
keyboardPlatform,
|
|
7
8
|
serializeAppWorkspaceLayoutState,
|
|
8
9
|
useLocale,
|
|
9
10
|
} from "@k2b/ui";
|
|
@@ -12,6 +13,7 @@ import { appWorkspaceCookieName, readAppWorkspaceLayoutCookie } from "../_intern
|
|
|
12
13
|
import { registerContextAwareCommand } from "../browser/command-bridge";
|
|
13
14
|
import { attachCommandShortcuts } from "../browser/command-shortcuts";
|
|
14
15
|
import { navigationSearchItems } from "../browser/navigation-search";
|
|
16
|
+
import { openPanel, type PanelRequest } from "../browser/panel";
|
|
15
17
|
import { importOnDemand } from "../browser/reload";
|
|
16
18
|
import { registerGlobalSearchHost } from "../browser/search-bridge";
|
|
17
19
|
import { attachShellFocusControls, readShellFocus, SHELL_FOCUS_CHANGE_EVENT, toggleShellFocus } from "../browser/shell-focus";
|
|
@@ -32,6 +34,8 @@ import { shellShortcuts } from "./shell-shortcuts";
|
|
|
32
34
|
import { syncTimezoneCookie } from "./timezone-cookie";
|
|
33
35
|
|
|
34
36
|
export type ShellRuntimeProps = {
|
|
37
|
+
/** This page has an authorized panel slot. */
|
|
38
|
+
panel?: boolean;
|
|
35
39
|
/** Layout pages outside focus mode: keep the current app's workspace layout in its cookie. */
|
|
36
40
|
workspace?: { appId?: string };
|
|
37
41
|
/** Layout pages: page titles, the way up (Alt+Up) and its per-tab memory. */
|
|
@@ -70,6 +74,61 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
|
|
|
70
74
|
onMount(() => syncTimezoneCookie(true));
|
|
71
75
|
onMount(() => onCleanup(installInputModeController()));
|
|
72
76
|
|
|
77
|
+
const togglePanel = () => {
|
|
78
|
+
window.dispatchEvent(new CustomEvent<PanelRequest>("cloud.panel.request", { detail: { action: "toggle" } }));
|
|
79
|
+
};
|
|
80
|
+
if (props.panel) {
|
|
81
|
+
onMount(() => {
|
|
82
|
+
let stopped = false;
|
|
83
|
+
let dispose: (() => void) | undefined;
|
|
84
|
+
const load = async (request?: PanelRequest) => {
|
|
85
|
+
const panel = await importOnDemand(() => import("../browser/panel-slot"));
|
|
86
|
+
if (!panel || stopped) return;
|
|
87
|
+
dispose ??= panel.installPanelSlot();
|
|
88
|
+
if (request) panel.handlePanelRequest(request);
|
|
89
|
+
};
|
|
90
|
+
const request = (event: CustomEvent<PanelRequest>) => {
|
|
91
|
+
const detail = event.detail;
|
|
92
|
+
if (detail.action === "open") {
|
|
93
|
+
if (detail.accepted || !readShellData()?.panel?.providers.some((provider) => provider.id === detail.provider)) return;
|
|
94
|
+
detail.accepted = true;
|
|
95
|
+
}
|
|
96
|
+
void load(event.detail);
|
|
97
|
+
};
|
|
98
|
+
const key = (event: KeyboardEvent) => {
|
|
99
|
+
const apple = keyboardPlatform() === "apple";
|
|
100
|
+
if (
|
|
101
|
+
event.defaultPrevented ||
|
|
102
|
+
event.isComposing ||
|
|
103
|
+
event.repeat ||
|
|
104
|
+
event.altKey ||
|
|
105
|
+
event.shiftKey ||
|
|
106
|
+
event.key.toLowerCase() !== "j" ||
|
|
107
|
+
event.metaKey !== apple ||
|
|
108
|
+
event.ctrlKey !== !apple
|
|
109
|
+
)
|
|
110
|
+
return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
togglePanel();
|
|
113
|
+
};
|
|
114
|
+
// The panel controller loads lazily; it reads this mark to recheck consent after a restore it did not see.
|
|
115
|
+
const restored = (event: PageTransitionEvent) => {
|
|
116
|
+
if (event.persisted) document.documentElement.setAttribute("data-cloud-page-restored", "");
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener("pageshow", restored);
|
|
119
|
+
window.addEventListener("cloud.panel.request", request);
|
|
120
|
+
if (!props.commands) window.addEventListener("keydown", key);
|
|
121
|
+
if (document.querySelector('.cloud-app-canvas[data-panel="open"]')) void load();
|
|
122
|
+
onCleanup(() => {
|
|
123
|
+
stopped = true;
|
|
124
|
+
dispose?.();
|
|
125
|
+
window.removeEventListener("pageshow", restored);
|
|
126
|
+
window.removeEventListener("cloud.panel.request", request);
|
|
127
|
+
window.removeEventListener("keydown", key);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
73
132
|
if (props.workspace) {
|
|
74
133
|
onMount(() => {
|
|
75
134
|
const appId = props.workspace?.appId;
|
|
@@ -146,15 +205,20 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
|
|
|
146
205
|
),
|
|
147
206
|
);
|
|
148
207
|
createEffect(() => {
|
|
149
|
-
for (const command of shellShortcuts(t())) {
|
|
208
|
+
for (const command of shellShortcuts(t(), { panel: props.panel })) {
|
|
150
209
|
onCleanup(
|
|
151
210
|
registerContextAwareCommand({
|
|
152
211
|
...command,
|
|
153
212
|
action:
|
|
154
|
-
command.id === "cloud.
|
|
155
|
-
?
|
|
156
|
-
:
|
|
157
|
-
|
|
213
|
+
command.id === "cloud.panel"
|
|
214
|
+
? togglePanel
|
|
215
|
+
: command.id === "cloud.search"
|
|
216
|
+
? { search: {} }
|
|
217
|
+
: // Returning the load keeps the Command busy until the dialog is open; a second press opens no second dialog.
|
|
218
|
+
() => {
|
|
219
|
+
if (openPanel("core")) return;
|
|
220
|
+
return importOnDemand(() => import("./LayoutHelp")).then((help) => help?.openLayoutHelpDialog());
|
|
221
|
+
},
|
|
158
222
|
}),
|
|
159
223
|
);
|
|
160
224
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppAppearance } from "../contracts/app";
|
|
2
|
+
import { appColorStyle } from "./app-color";
|
|
2
3
|
import { resolveRuntimeRoute } from "./app-navigation";
|
|
3
4
|
import { helpPageAppId } from "./layout-help-url";
|
|
4
5
|
import type { RuntimeContext } from "./runtime";
|
|
@@ -15,11 +16,11 @@ export const appAppearanceStyle = (appearance: AppAppearance | undefined): strin
|
|
|
15
16
|
const angle = Number.isFinite(appearance.background?.angle) ? Math.max(0, Math.min(360, appearance.background?.angle ?? 135)) : 135;
|
|
16
17
|
const strength = clampStrength(appearance.background?.strength, 20);
|
|
17
18
|
const darkStrength = Math.round(strength / 2);
|
|
18
|
-
return `--app-accent:${appearance.accent};--app-canvas-from:${from};--app-canvas-via:${via};--app-canvas-to:${to};--app-canvas-angle:${angle}deg;--app-canvas-strength:${strength}%;--app-canvas-dark-strength:${darkStrength}%`;
|
|
19
|
+
return `--app-accent:${appearance.accent};${appColorStyle(appearance.accent)};--app-canvas-from:${from};--app-canvas-via:${via};--app-canvas-to:${to};--app-canvas-angle:${angle}deg;--app-canvas-strength:${strength}%;--app-canvas-dark-strength:${darkStrength}%`;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export const appAccentStyle = (accent: string | undefined): string | undefined =>
|
|
22
|
-
isHexColor(accent) ? `--app-accent:${accent}` : undefined;
|
|
23
|
+
isHexColor(accent) ? `--app-accent:${accent};${appColorStyle(accent)}` : undefined;
|
|
23
24
|
|
|
24
25
|
/** Core's full-page Help of an app belongs to that app. */
|
|
25
26
|
export const resolveCurrentApp = (apps: RuntimeContext["apps"], pathname: string) => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
type HexColor = `#${string}`;
|
|
2
|
+
|
|
3
|
+
const minimumContrast = 4.5;
|
|
4
|
+
const fillShares = {
|
|
5
|
+
light: { selected: 0.07, selectedNav: 0.1 },
|
|
6
|
+
dark: { selected: 0.14, selectedNav: 0.17 },
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Opaque theme backgrounds checked for readable app ink; a test pins them to
|
|
11
|
+
* tokens.css and @k2b/ui. `sidebar` is the workspace sidebar's own background
|
|
12
|
+
* (`--k2b-surface` since the one-surface workspace); a test reads that
|
|
13
|
+
* declaration from @k2b/ui. `sidebarHover` is its row hover over it; `subtle`,
|
|
14
|
+
* `muted`, `hover`, and `platformSelected` are the `--ui-surface-subtle`,
|
|
15
|
+
* `--ui-surface-muted`, `--ui-hover`, and blue `--ui-selected` fills app ink
|
|
16
|
+
* also sits on.
|
|
17
|
+
*/
|
|
18
|
+
export const appColorSurfaces = {
|
|
19
|
+
light: {
|
|
20
|
+
canvas: "#fafafa",
|
|
21
|
+
surface: "#ffffff",
|
|
22
|
+
sidebar: "#ffffff",
|
|
23
|
+
sidebarHover: "#f3f3f3",
|
|
24
|
+
subtle: "#f8f8f8",
|
|
25
|
+
muted: "#f3f4f3",
|
|
26
|
+
raised: "#ffffff",
|
|
27
|
+
hover: "#f0f1f0",
|
|
28
|
+
platformSelected: "#f0f6ff",
|
|
29
|
+
},
|
|
30
|
+
dark: {
|
|
31
|
+
canvas: "#090d12",
|
|
32
|
+
surface: "#11151b",
|
|
33
|
+
sidebar: "#11151b",
|
|
34
|
+
sidebarHover: "#1d2026",
|
|
35
|
+
subtle: "#161a20",
|
|
36
|
+
muted: "#20242a",
|
|
37
|
+
raised: "#181d24",
|
|
38
|
+
hover: "#252c35",
|
|
39
|
+
platformSelected: "#192636",
|
|
40
|
+
},
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const channels = (hex: HexColor) =>
|
|
44
|
+
[Number.parseInt(hex.slice(1, 3), 16), Number.parseInt(hex.slice(3, 5), 16), Number.parseInt(hex.slice(5, 7), 16)] as const;
|
|
45
|
+
|
|
46
|
+
/** WCAG 2.x relative luminance from a six-digit sRGB hex color. */
|
|
47
|
+
export const relativeLuminance = (hex: HexColor): number => {
|
|
48
|
+
const linearize = (value: number) => {
|
|
49
|
+
const c = value / 255;
|
|
50
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
};
|
|
52
|
+
const [r, g, b] = channels(hex);
|
|
53
|
+
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** WCAG 2.x contrast ratio between two opaque hex colors. */
|
|
57
|
+
export const contrastRatio = (a: HexColor, b: HexColor): number => {
|
|
58
|
+
const first = relativeLuminance(a);
|
|
59
|
+
const second = relativeLuminance(b);
|
|
60
|
+
return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Rounded sRGB channel mix, with share indicating the fraction of a. */
|
|
64
|
+
export const mixHex = (a: HexColor, b: HexColor, share: number): HexColor => {
|
|
65
|
+
const second = channels(b);
|
|
66
|
+
const fraction = Math.max(0, Math.min(1, share));
|
|
67
|
+
return `#${channels(a)
|
|
68
|
+
.map((value, index) =>
|
|
69
|
+
Math.round(value * fraction + second[index]! * (1 - fraction))
|
|
70
|
+
.toString(16)
|
|
71
|
+
.padStart(2, "0"),
|
|
72
|
+
)
|
|
73
|
+
.join("")}`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Derive selection fills, the shell's open-app fill, and the nearest AA-readable app ink in each theme. */
|
|
77
|
+
export const appColors = (accent: HexColor) => {
|
|
78
|
+
const forTheme = (theme: keyof typeof appColorSurfaces) => {
|
|
79
|
+
const surfaces = appColorSurfaces[theme];
|
|
80
|
+
const selected = mixHex(accent, surfaces.surface, fillShares[theme].selected);
|
|
81
|
+
const selectedNav = mixHex(accent, surfaces.sidebar, fillShares[theme].selectedNav);
|
|
82
|
+
// Cloud's shell marks the open app on its app-colored bar (global.css):
|
|
83
|
+
// with the surface in light, and in dark with the navigation share over the
|
|
84
|
+
// muted surface, which stays apart from the dark bar.
|
|
85
|
+
const shellSelected = theme === "light" ? surfaces.surface : mixHex(accent, surfaces.muted, fillShares.dark.selectedNav);
|
|
86
|
+
const backgrounds = [...Object.values(surfaces), selected, selectedNav, shellSelected];
|
|
87
|
+
const toward = theme === "light" ? "#000000" : "#ffffff";
|
|
88
|
+
let ink: HexColor = toward;
|
|
89
|
+
for (let i = 100; i >= 0; i--) {
|
|
90
|
+
const candidate = mixHex(accent, toward, i / 100);
|
|
91
|
+
if (backgrounds.every((background) => contrastRatio(candidate, background) >= minimumContrast)) {
|
|
92
|
+
ink = candidate;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { ink, selected, selectedNav, shellSelected };
|
|
97
|
+
};
|
|
98
|
+
return { light: forTheme("light"), dark: forTheme("dark") };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Plain hex declarations for server-rendered app appearance and accent scopes. */
|
|
102
|
+
export const appColorStyle = (accent: HexColor): string => {
|
|
103
|
+
const { light, dark } = appColors(accent);
|
|
104
|
+
return `--app-ink-light:${light.ink};--app-ink-dark:${dark.ink};--app-selected-light:${light.selected};--app-selected-dark:${dark.selected};--app-selected-nav-light:${light.selectedNav};--app-selected-nav-dark:${dark.selectedNav}`;
|
|
105
|
+
};
|
|
@@ -66,7 +66,7 @@ export function AppLaunchpadProvider(props: AppLaunchpadContext) {
|
|
|
66
66
|
return <span class="hidden" data-cloud-app-launchpad-provider />;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export function AppLaunchpadButton(props: AppLaunchpadContext & { variant: "rail" | "header" | "menu"; label?: string }) {
|
|
69
|
+
export function AppLaunchpadButton(props: AppLaunchpadContext & { variant: "rail" | "header" | "title" | "menu"; label?: string }) {
|
|
70
70
|
const locale = useLocale();
|
|
71
71
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
72
72
|
const action = createBusyAction();
|
|
@@ -98,6 +98,23 @@ export function AppLaunchpadButton(props: AppLaunchpadContext & { variant: "rail
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// The page title as the app menu's trigger on phones; global.css shows it only when the page has app navigation.
|
|
102
|
+
if (props.variant === "title") {
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="layout-header-title-menu"
|
|
107
|
+
aria-haspopup="dialog"
|
|
108
|
+
onClick={open}
|
|
109
|
+
aria-busy={action.busy() ? "true" : undefined}
|
|
110
|
+
>
|
|
111
|
+
<span class="truncate font-semibold">{props.label}</span>
|
|
112
|
+
<span class="sr-only">{`, ${t().appMenu}`}</span>
|
|
113
|
+
<i class={action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-chevron-down"} aria-hidden="true" />
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
if (props.variant === "header") {
|
|
102
119
|
return (
|
|
103
120
|
<IconButton label={props.label ?? t().openApps} onClick={open} aria-busy={action.busy() ? "true" : undefined}>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { isOwnAppPath } from "../_internal/app-path";
|
|
1
2
|
import { hasAnyAppRole } from "../_internal/app-roles";
|
|
2
3
|
import type { RuntimeAppMeta } from "../contracts/app";
|
|
4
|
+
import { panelProviderNeedsConsent } from "../contracts/panel-context-plain";
|
|
3
5
|
import { isPwaPartId } from "../contracts/pwa-paths";
|
|
4
6
|
import type { User } from "../contracts/shared";
|
|
5
7
|
import { resolveAppPresentation } from "../shared/app-presentation";
|
|
@@ -52,3 +54,29 @@ export const visiblePwaParts = (apps: readonly RuntimeAppMeta[], user: User | un
|
|
|
52
54
|
.map((app) => resolveAppPresentation(app, locale))
|
|
53
55
|
.sort((a, b) => a.name.localeCompare(b.name, locale) || a.id.localeCompare(b.id));
|
|
54
56
|
};
|
|
57
|
+
|
|
58
|
+
export type PanelProvider = { id: string; label: string; icon: string; href: string; appName: string; consent: boolean };
|
|
59
|
+
|
|
60
|
+
/** Authorized presentation only: the panel route still authorizes every request. */
|
|
61
|
+
export const visiblePanelProviders = (apps: readonly RuntimeAppMeta[], user: User | undefined, locale: string): PanelProvider[] => {
|
|
62
|
+
if (!user) return [];
|
|
63
|
+
return apps
|
|
64
|
+
.flatMap((app) => {
|
|
65
|
+
if (!app.panel || !isOwnAppPath(app.panel.href, app.routes) || !hasAnyAppRole(user, app.panel.requiresRoles)) return [];
|
|
66
|
+
const presentation = resolveAppPresentation(app, locale);
|
|
67
|
+
const panel = presentation.panel;
|
|
68
|
+
return panel
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
id: app.id,
|
|
72
|
+
label: panel.label,
|
|
73
|
+
icon: panel.icon,
|
|
74
|
+
href: panel.href,
|
|
75
|
+
appName: presentation.name,
|
|
76
|
+
consent: panelProviderNeedsConsent(app.id),
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
: [];
|
|
80
|
+
})
|
|
81
|
+
.sort((a, b) => a.label.localeCompare(b.label, locale) || a.id.localeCompare(b.id));
|
|
82
|
+
};
|