@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
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { HELP_VIEW_LIMIT, type HelpKind } from "../shared/help";
|
|
2
2
|
import { SHELL_SHORTCUT_ORDER } from "./shell-shortcuts";
|
|
3
3
|
|
|
4
4
|
/** The concept asks for at most eight tasks as tiles; more task articles follow as a list. */
|
|
5
5
|
export const HELP_START_TILE_LIMIT = 8;
|
|
6
6
|
/** "For this view" shows two or three articles the page names. */
|
|
7
|
-
export
|
|
7
|
+
export { HELP_VIEW_LIMIT } from "../shared/help";
|
|
8
8
|
export const HELP_START_GROUP_KINDS = ["start", "reference", "troubleshoot"] as const;
|
|
9
9
|
export type HelpStartGroupKind = (typeof HELP_START_GROUP_KINDS)[number];
|
|
10
10
|
export type HelpStartTopic = { id: string; title: string; order?: number; kind?: HelpKind };
|
|
@@ -41,19 +41,42 @@ export const helpStart = <T extends HelpStartTopic>(topics: readonly T[], viewId
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export type HelpShortcutScope = "everywhere" | "app" | "selection";
|
|
44
|
+
// Three groups, each as scannable as HELP_START_TILE_LIMIT (8) task tiles: 3 x 8 rows.
|
|
45
|
+
// Today's largest real page is well below this (shell 3 + Spaces about 8).
|
|
46
|
+
export const HELP_START_SHORTCUT_LIMIT = 24;
|
|
47
|
+
// Qualified command ids share CAPABILITY_QUALIFIED_ID_MAX_LENGTH.
|
|
48
|
+
export const HELP_SHORTCUT_ID_MAX_LENGTH = 180;
|
|
49
|
+
// One row label: the platform's CapabilityLink title / human-readable label bound.
|
|
50
|
+
export const HELP_SHORTCUT_TITLE_MAX_LENGTH = 120;
|
|
51
|
+
// Row tooltip: the platform's plain-text Action review message bound.
|
|
52
|
+
export const HELP_SHORTCUT_DESCRIPTION_MAX_LENGTH = 1000;
|
|
53
|
+
// command-shortcuts grammar: five distinct modifier tokens "ctrl+alt+shift+meta+mod+" (24 chars)
|
|
54
|
+
// plus the longest standard KeyboardEvent.key name "TVAudioDescriptionMixDown" (25 chars).
|
|
55
|
+
export const HELP_SHORTCUT_KEYS_MAX_LENGTH = 49;
|
|
56
|
+
export type HelpShortcut = { id: string; title: string; description?: string; shortcut: string; scope?: "selection" | "page" };
|
|
44
57
|
export type HelpShortcutCommand = { id: string; title: string; description?: string; shortcut?: string; scope?: "selection" | "page" };
|
|
45
58
|
/**
|
|
46
59
|
* Group commands with a shortcut: scope "selection" -> "selection"; id starting with "cloud." -> "everywhere"; the rest -> "app".
|
|
47
60
|
* Group order everywhere, app, selection; empty groups are absent. Inside "everywhere", the ids of SHELL_SHORTCUT_ORDER come first in that
|
|
48
61
|
* order (so rows rendered on the server keep their place when the browser adds more), then the rest in input order; other groups keep input order.
|
|
49
|
-
* Commands without a shortcut are dropped.
|
|
62
|
+
* Commands without a shortcut, with empty ids/titles or exceeding the string bounds are dropped.
|
|
63
|
+
* Keep at most HELP_START_SHORTCUT_LIMIT rows across groups in group order.
|
|
50
64
|
*/
|
|
51
65
|
export const helpShortcutGroups = <T extends HelpShortcutCommand>(
|
|
52
66
|
commands: readonly T[],
|
|
53
67
|
): { scope: HelpShortcutScope; shortcuts: (T & { shortcut: string })[] }[] => {
|
|
54
68
|
const groups: Record<HelpShortcutScope, (T & { shortcut: string })[]> = { everywhere: [], app: [], selection: [] };
|
|
55
69
|
for (const command of commands) {
|
|
56
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
!command.shortcut ||
|
|
72
|
+
!command.id ||
|
|
73
|
+
!command.title ||
|
|
74
|
+
command.id.length > HELP_SHORTCUT_ID_MAX_LENGTH ||
|
|
75
|
+
command.title.length > HELP_SHORTCUT_TITLE_MAX_LENGTH ||
|
|
76
|
+
(command.description?.length ?? 0) > HELP_SHORTCUT_DESCRIPTION_MAX_LENGTH ||
|
|
77
|
+
command.shortcut.length > HELP_SHORTCUT_KEYS_MAX_LENGTH
|
|
78
|
+
)
|
|
79
|
+
continue;
|
|
57
80
|
const scope = command.scope === "selection" ? "selection" : command.id.startsWith("cloud.") ? "everywhere" : "app";
|
|
58
81
|
groups[scope].push({ ...command, shortcut: command.shortcut });
|
|
59
82
|
}
|
|
@@ -62,7 +85,10 @@ export const helpShortcutGroups = <T extends HelpShortcutCommand>(
|
|
|
62
85
|
return index < 0 ? SHELL_SHORTCUT_ORDER.length : index;
|
|
63
86
|
};
|
|
64
87
|
groups.everywhere.sort((a, b) => rank(a.id) - rank(b.id));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
let remaining = HELP_START_SHORTCUT_LIMIT;
|
|
89
|
+
return (["everywhere", "app", "selection"] as const).flatMap((scope) => {
|
|
90
|
+
const shortcuts = groups[scope].slice(0, remaining);
|
|
91
|
+
remaining -= shortcuts.length;
|
|
92
|
+
return shortcuts.length ? [{ scope, shortcuts }] : [];
|
|
93
|
+
});
|
|
68
94
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { readPanelCookie } from "../_internal/panel-cookie";
|
|
3
|
+
import { getLocale } from "../server/locale";
|
|
4
|
+
import { panelContextConsent } from "../services/panel-context-consent";
|
|
5
|
+
import { visiblePanelProviders } from "./app-navigation";
|
|
6
|
+
import { getRuntimeContext } from "./runtime";
|
|
7
|
+
|
|
8
|
+
/** One read, only for a host page with the slot open on a consent provider. Fail closed when the store is unavailable. */
|
|
9
|
+
export async function preloadPanelConsent(c: Context): Promise<void> {
|
|
10
|
+
const user = c.get("user");
|
|
11
|
+
if (!user || !c.get("runtime")) return;
|
|
12
|
+
const providers = visiblePanelProviders(getRuntimeContext(c).apps, user, getLocale(c));
|
|
13
|
+
const state = readPanelCookie(
|
|
14
|
+
c.req.header("Cookie"),
|
|
15
|
+
providers.map((provider) => provider.id),
|
|
16
|
+
);
|
|
17
|
+
const provider = state.open && providers.find((provider) => provider.id === state.provider);
|
|
18
|
+
if (!provider || !provider.consent) return;
|
|
19
|
+
// The provider's own page below its panel path renders in PanelLayout, which has no slot and never asks.
|
|
20
|
+
const scope = new URL(provider.href, "https://panel.invalid").pathname.replace(/\/$/, "");
|
|
21
|
+
if (c.req.path === scope || c.req.path.startsWith(`${scope}/`)) return;
|
|
22
|
+
let answer: { allowed: boolean; revision: number } | null = null;
|
|
23
|
+
try {
|
|
24
|
+
answer = await panelContextConsent.get(user.id, provider.id);
|
|
25
|
+
} catch {
|
|
26
|
+
// The shell shows the unanswered question, without context in the URL or messages.
|
|
27
|
+
}
|
|
28
|
+
c.set("panelConsent", { provider: provider.id, allowed: answer?.allowed ?? null, revision: answer?.revision ?? 0 });
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PANEL_CONTEXT_PARAM, type PanelPageContext, parsePanelContextParam } from "../contracts/panel-context";
|
|
2
|
+
|
|
3
|
+
/** Untrusted URL input: a selector only. Read through your server using the session's actor/accessSubject. */
|
|
4
|
+
export function readPanelContext(c: { req: { raw: { url: string } } }): PanelPageContext | null {
|
|
5
|
+
try {
|
|
6
|
+
return parsePanelContextParam(new URL(c.req.raw.url).searchParams.get(PANEL_CONTEXT_PARAM));
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { AppWorkspace } 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 { createMePanelConsentRoutes } from "../api/me-panel-consent";
|
|
6
|
+
import type { RuntimeAppMeta } from "../contracts/app";
|
|
7
|
+
import type { User } from "../contracts/shared";
|
|
8
|
+
import Layout from "./Layout";
|
|
9
|
+
import PanelLayout from "./PanelLayout";
|
|
10
|
+
import { readPanelContext } from "./panel-context";
|
|
11
|
+
import type { RuntimeContext } from "./runtime";
|
|
12
|
+
|
|
13
|
+
type ConsentAnswer = { allowed: boolean; revision: number };
|
|
14
|
+
export const consentAnswers = new Map<string, Map<string, ConsentAnswer>>();
|
|
15
|
+
export const fixtureConsent = {
|
|
16
|
+
get: async (userId: string, provider: string) => consentAnswers.get(userId)?.get(provider) ?? null,
|
|
17
|
+
list: async (userId: string, providers: string[]) =>
|
|
18
|
+
Object.fromEntries(
|
|
19
|
+
providers.flatMap((id) => {
|
|
20
|
+
const answer = consentAnswers.get(userId)?.get(id);
|
|
21
|
+
return answer === undefined ? [] : [[id, answer]];
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
set: async (userId: string, provider: string, allowed: boolean, revision: number) => {
|
|
25
|
+
const answers = consentAnswers.get(userId) ?? new Map<string, ConsentAnswer>();
|
|
26
|
+
if ((answers.get(provider)?.revision ?? 0) !== revision) return null;
|
|
27
|
+
const saved = { allowed, revision: revision + 1 };
|
|
28
|
+
answers.set(provider, saved);
|
|
29
|
+
consentAnswers.set(userId, answers);
|
|
30
|
+
return saved;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const apps: RuntimeAppMeta[] = ["fixture", "second"].map((id) => ({
|
|
35
|
+
id,
|
|
36
|
+
name: id === "fixture" ? "Fixture" : "Second",
|
|
37
|
+
icon: "ti ti-box",
|
|
38
|
+
description: "Panel browser fixture",
|
|
39
|
+
routes: [`/app/${id}`],
|
|
40
|
+
nav: { href: `/app/${id}`, section: "primary" },
|
|
41
|
+
panel: { href: `/app/${id}/panel`, label: id === "fixture" ? "Fixture" : "Second", icon: "ti ti-box" },
|
|
42
|
+
}));
|
|
43
|
+
const probe = defineApp({
|
|
44
|
+
id: "fixture",
|
|
45
|
+
name: "Fixture",
|
|
46
|
+
icon: "ti ti-box",
|
|
47
|
+
description: "Panel browser fixture",
|
|
48
|
+
baseUrl: "http://fixture:3000",
|
|
49
|
+
routes: ["/app/fixture", "/app/second"],
|
|
50
|
+
});
|
|
51
|
+
type Environment = { Variables: { runtime: RuntimeContext; user: User } };
|
|
52
|
+
export const server = new Hono<Environment>()
|
|
53
|
+
.use(async (c, next) => {
|
|
54
|
+
c.set("runtime", { apps });
|
|
55
|
+
const id = c.req
|
|
56
|
+
.header("cookie")
|
|
57
|
+
?.split(";")
|
|
58
|
+
.map((value) => value.trim())
|
|
59
|
+
.find((value) => value.startsWith("fixture_person="))
|
|
60
|
+
?.slice("fixture_person=".length);
|
|
61
|
+
c.set("user", { ...person, ...(id ? { id } : {}) });
|
|
62
|
+
await next();
|
|
63
|
+
})
|
|
64
|
+
.route("/api/me", createMePanelConsentRoutes(fixtureConsent))
|
|
65
|
+
.get(
|
|
66
|
+
"/app/:provider/panel",
|
|
67
|
+
...probe.ssr<Environment>((c) => () => (
|
|
68
|
+
<PanelLayout c={c} title="Fixture panel">
|
|
69
|
+
<output id="panel-context-probe">{JSON.stringify(readPanelContext(c))}</output>
|
|
70
|
+
<label>
|
|
71
|
+
Panel input
|
|
72
|
+
<input aria-label="Panel input" />
|
|
73
|
+
</label>
|
|
74
|
+
<button type="button" id="prevent-escape">
|
|
75
|
+
Keep Escape
|
|
76
|
+
</button>
|
|
77
|
+
<a href="/app/fixture/next" id="leave-panel">
|
|
78
|
+
Open in the app
|
|
79
|
+
</a>
|
|
80
|
+
</PanelLayout>
|
|
81
|
+
)),
|
|
82
|
+
)
|
|
83
|
+
.get(
|
|
84
|
+
"/app/fixture/long",
|
|
85
|
+
...probe.ssr<Environment>((c) => () => (
|
|
86
|
+
<Layout context={{ scope: { type: "fixture.place", id: "place1" } }} c={c} title="Long fixture">
|
|
87
|
+
<button type="button" id="open-panel">
|
|
88
|
+
Open fixture
|
|
89
|
+
</button>
|
|
90
|
+
<button type="button" id="open-silent-panel">
|
|
91
|
+
Open silent fixture
|
|
92
|
+
</button>
|
|
93
|
+
<div style="height:150vh">Long content</div>
|
|
94
|
+
<p id="last-row">Last row</p>
|
|
95
|
+
</Layout>
|
|
96
|
+
)),
|
|
97
|
+
)
|
|
98
|
+
.get(
|
|
99
|
+
"/app/fixture/:page?",
|
|
100
|
+
...probe.ssr<Environment>((c) => () => (
|
|
101
|
+
<Layout context={{ scope: { type: "fixture.place", id: "place1" } }} c={c} fullWidth fullPage title="Panel fixture">
|
|
102
|
+
<AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
|
|
103
|
+
<AppWorkspace.Sidebar id="panel-navigation" label="Navigation">
|
|
104
|
+
<AppWorkspace.SidebarDesktop>
|
|
105
|
+
<AppWorkspace.SidebarItem href="/app/fixture" active>
|
|
106
|
+
Workspace
|
|
107
|
+
</AppWorkspace.SidebarItem>
|
|
108
|
+
</AppWorkspace.SidebarDesktop>
|
|
109
|
+
</AppWorkspace.Sidebar>
|
|
110
|
+
<AppWorkspace.Content>
|
|
111
|
+
<AppWorkspace.Main>
|
|
112
|
+
<button type="button" id="open-panel">
|
|
113
|
+
Open fixture
|
|
114
|
+
</button>
|
|
115
|
+
<button type="button" id="open-silent-panel">
|
|
116
|
+
Open silent fixture
|
|
117
|
+
</button>
|
|
118
|
+
<a href="/app/fixture/next" id="next-page">
|
|
119
|
+
Next page
|
|
120
|
+
</a>
|
|
121
|
+
<button type="button" id="change-context">
|
|
122
|
+
Change place
|
|
123
|
+
</button>
|
|
124
|
+
<textarea aria-label="App editor" />
|
|
125
|
+
</AppWorkspace.Main>
|
|
126
|
+
<AppWorkspace.Detail id="panel-fixture-detail" width="lg" open>
|
|
127
|
+
<h2>Selected item</h2>
|
|
128
|
+
<button type="button" data-workspace-detail-close>
|
|
129
|
+
Close detail
|
|
130
|
+
</button>
|
|
131
|
+
</AppWorkspace.Detail>
|
|
132
|
+
</AppWorkspace.Content>
|
|
133
|
+
</AppWorkspace>
|
|
134
|
+
</Layout>
|
|
135
|
+
)),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/** Runs inline during parsing and again immediately after mounting the real runtime. */
|
|
139
|
+
export function panelGeometry() {
|
|
140
|
+
return [
|
|
141
|
+
".layout-shell-content",
|
|
142
|
+
"#cloud-panel",
|
|
143
|
+
".k2b-app-workspace",
|
|
144
|
+
".k2b-app-workspace__main",
|
|
145
|
+
".k2b-app-workspace__detail",
|
|
146
|
+
"[data-app-workspace-detail-toggle]",
|
|
147
|
+
].map((selector) => {
|
|
148
|
+
const rect = document.querySelector(selector)?.getBoundingClientRect();
|
|
149
|
+
return rect && { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The question, provider frame and app area stay in the same reserved rectangles through hydration. */
|
|
154
|
+
export function consentGeometry() {
|
|
155
|
+
return ["[data-cloud-panel-consent]", "[data-cloud-panel-frame]", "[data-cloud-app-area]"].map((selector) => {
|
|
156
|
+
const rect = document.querySelector(selector)?.getBoundingClientRect();
|
|
157
|
+
return rect && { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -4,6 +4,30 @@ export const platformMessages = i18n.define({
|
|
|
4
4
|
baseLocale: "en",
|
|
5
5
|
messages: {
|
|
6
6
|
en: {
|
|
7
|
+
panel: "Panel",
|
|
8
|
+
panels: "Panels",
|
|
9
|
+
openPanel: "Open panel",
|
|
10
|
+
closePanel: "Close panel",
|
|
11
|
+
togglePanel: "Open or close the panel",
|
|
12
|
+
togglePanelDescription: "Shows a panel beside the app.",
|
|
13
|
+
panelShortcut: ({ mod }: { mod: string }) => `Panel (${mod}J)`,
|
|
14
|
+
resizePanel: "Resize panel",
|
|
15
|
+
releaseClosesPanel: "Release to close the panel",
|
|
16
|
+
panelLoadFailed: "The panel did not load",
|
|
17
|
+
panelNotResponding: ({ label }: { label: string }) => `${label} is not responding.`,
|
|
18
|
+
retry: "Try again",
|
|
19
|
+
panelLoading: ({ label }: { label: string }) => `${label} is loading`,
|
|
20
|
+
enlargePanel: "Enlarge panel",
|
|
21
|
+
shrinkPanel: "Shrink panel",
|
|
22
|
+
panelConsentTitle: ({ app }: { app: string }) => `May ${app} see where you are?`,
|
|
23
|
+
panelConsentBody: ({ app }: { app: string }) =>
|
|
24
|
+
`${app} only learns which place you are at. It can read only what you may read yourself. You can change this in the ⋯ menu at any time.`,
|
|
25
|
+
panelConsentAllow: "Allow",
|
|
26
|
+
panelConsentDeny: "Don't share",
|
|
27
|
+
panelMenu: "Panel menu",
|
|
28
|
+
panelFrom: ({ app }: { app: string }) => `Panel from ${app}`,
|
|
29
|
+
panelShareContext: "Share context",
|
|
30
|
+
panelConsentSaveError: "Not saved. Try again.",
|
|
7
31
|
printFallbackTitle: "This view does not print well.",
|
|
8
32
|
printFallbackHint: "Open the item you want to print, or use the app's export.",
|
|
9
33
|
admin: "Admin",
|
|
@@ -78,6 +102,30 @@ export const platformMessages = i18n.define({
|
|
|
78
102
|
clearSearch: "Clear search",
|
|
79
103
|
},
|
|
80
104
|
de: {
|
|
105
|
+
panel: "Panel",
|
|
106
|
+
panels: "Panels",
|
|
107
|
+
openPanel: "Panel öffnen",
|
|
108
|
+
closePanel: "Panel schließen",
|
|
109
|
+
togglePanel: "Panel öffnen oder schließen",
|
|
110
|
+
togglePanelDescription: "Zeigt ein Panel neben der App.",
|
|
111
|
+
panelShortcut: ({ mod }: { mod: string }) => `Panel (${mod}J)`,
|
|
112
|
+
resizePanel: "Panelbreite ändern",
|
|
113
|
+
releaseClosesPanel: "Loslassen schließt das Panel",
|
|
114
|
+
panelLoadFailed: "Das Panel lädt nicht",
|
|
115
|
+
panelNotResponding: ({ label }: { label: string }) => `${label} antwortet nicht.`,
|
|
116
|
+
retry: "Erneut versuchen",
|
|
117
|
+
panelLoading: ({ label }: { label: string }) => `${label} lädt`,
|
|
118
|
+
enlargePanel: "Panel vergrößern",
|
|
119
|
+
shrinkPanel: "Panel verkleinern",
|
|
120
|
+
panelConsentTitle: ({ app }: { app: string }) => `Darf ${app} sehen, wo du gerade bist?`,
|
|
121
|
+
panelConsentBody: ({ app }: { app: string }) =>
|
|
122
|
+
`${app} erfährt nur, an welchem Ort du gerade bist. Lesen kann es nur, was du selbst lesen darfst. Du kannst das im Menü ⋯ jederzeit ändern.`,
|
|
123
|
+
panelConsentAllow: "Erlauben",
|
|
124
|
+
panelConsentDeny: "Nicht teilen",
|
|
125
|
+
panelMenu: "Panel-Menü",
|
|
126
|
+
panelFrom: ({ app }: { app: string }) => `Panel von ${app}`,
|
|
127
|
+
panelShareContext: "Kontext teilen",
|
|
128
|
+
panelConsentSaveError: "Nicht gespeichert. Versuche es erneut.",
|
|
81
129
|
printFallbackTitle: "Diese Ansicht lässt sich nicht gut drucken.",
|
|
82
130
|
printFallbackHint: "Öffne den Eintrag, den du drucken möchtest, oder nutze den Export der App.",
|
|
83
131
|
admin: "Administration",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TabBarItem } from "@k2b/ui";
|
|
1
2
|
import type { RailPreferences, RailSnapshot } from "../contracts/rail-preferences";
|
|
2
3
|
import { helpPageAppId } from "./layout-help-url";
|
|
3
4
|
|
|
@@ -66,3 +67,46 @@ export const railLinkActive = (link: RailLink, currentPath: string): boolean =>
|
|
|
66
67
|
(target.pathname !== "/" && current.pathname.startsWith(`${target.pathname.replace(/\/$/, "")}/`))
|
|
67
68
|
);
|
|
68
69
|
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The phone bar's apps: the rail's links in the person's order, at most `limit`. The current page's link stays in the
|
|
73
|
+
* bar; when it comes later in the rail, it takes the last place, so the bar always marks where the person is.
|
|
74
|
+
*/
|
|
75
|
+
export const phoneBarLinks = (navigation: { shortcuts: RailLink[]; apps: RailLink[] }, currentPath: string, limit: number): RailLink[] => {
|
|
76
|
+
const links = [...navigation.shortcuts, ...navigation.apps];
|
|
77
|
+
const current = links.findIndex((link) => railLinkActive(link, currentPath));
|
|
78
|
+
if (current < limit) return links.slice(0, limit);
|
|
79
|
+
return [...links.slice(0, limit - 1), links[current]!];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Start and four apps fill the phone bar's five places. */
|
|
83
|
+
const PHONE_BAR_APPS = 4;
|
|
84
|
+
|
|
85
|
+
const underPath = (pathname: string, path: string) => pathname === path || pathname.startsWith(`${path.replace(/\/$/, "")}/`);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The phone bar: Home, then the person's first apps in their rail order. Home leads to `/`, which opens the
|
|
89
|
+
* installation's start page (`home`), so it is the open item on `/` and on that start page, unless an app in the bar
|
|
90
|
+
* is open there.
|
|
91
|
+
*/
|
|
92
|
+
export const phoneBarItems = (
|
|
93
|
+
navigation: { shortcuts: RailLink[]; apps: RailLink[] },
|
|
94
|
+
currentPath: string,
|
|
95
|
+
home: { label: string; path: string },
|
|
96
|
+
): TabBarItem[] => {
|
|
97
|
+
const links = phoneBarLinks(navigation, currentPath, PHONE_BAR_APPS);
|
|
98
|
+
const items = links.map((link) => ({
|
|
99
|
+
id: link.id,
|
|
100
|
+
label: link.label,
|
|
101
|
+
icon: link.iconClass,
|
|
102
|
+
href: link.href,
|
|
103
|
+
current: railLinkActive(link, currentPath),
|
|
104
|
+
}));
|
|
105
|
+
const pathname = new URL(currentPath, "https://cloud.invalid").pathname;
|
|
106
|
+
const start = new URL(home.path, "https://cloud.invalid").pathname;
|
|
107
|
+
const atHome = pathname === "/" || (!items.some((item) => item.current) && underPath(pathname, start));
|
|
108
|
+
return [
|
|
109
|
+
{ id: "home", label: home.label, icon: "ti ti-home", href: "/", current: atHome },
|
|
110
|
+
...items.map((item) => ({ ...item, current: !atHome && item.current })),
|
|
111
|
+
];
|
|
112
|
+
};
|
package/src/ssr/shell-data.tsx
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { KeyboardPlatform } from "@k2b/ui";
|
|
2
2
|
import { createContext, type JSX, useContext } from "solid-js";
|
|
3
3
|
import type { AppSearchLink } from "../contracts/app";
|
|
4
|
+
import type { PanelConsentSnapshot, PanelPageContext } from "../contracts/panel-context";
|
|
4
5
|
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
5
6
|
import type { CloudThemePreference } from "../shared/theme";
|
|
6
7
|
import type { AppLaunchpadLegalLink } from "./app-launchpad";
|
|
8
|
+
import type { PanelProvider } from "./app-navigation";
|
|
7
9
|
import type { RailApp } from "./rail-navigation";
|
|
8
10
|
|
|
9
11
|
export type ShellData = {
|
|
12
|
+
panel?: { providers: PanelProvider[]; provider?: string; context: PanelPageContext | null; consent?: PanelConsentSnapshot };
|
|
10
13
|
/** The authorized navigation catalog, sorted for the request locale: the rail, the app grid and the phone menu show it. */
|
|
11
14
|
apps: RailApp[];
|
|
12
15
|
/** Signed-in pages: the person's rail preferences. */
|
|
13
16
|
rail?: RailSnapshot;
|
|
17
|
+
/** Signed-in pages: the start page that `/` opens, for the phone bar's Home. */
|
|
18
|
+
home?: string;
|
|
14
19
|
legalLinks: AppLaunchpadLegalLink[];
|
|
15
20
|
/** Signed-in pages: the person, for the profile menu and the phone menu's profile actions. */
|
|
16
21
|
profile?: { name: string; avatarSrc?: string; appHref?: string };
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { platformMessages } from "./platform-messages";
|
|
2
2
|
|
|
3
|
-
export const SHELL_SHORTCUT_ORDER = ["cloud.search", "cloud.help"] as const;
|
|
3
|
+
export const SHELL_SHORTCUT_ORDER = ["cloud.search", "cloud.help", "cloud.panel"] as const;
|
|
4
4
|
/** The shell's always-present shortcuts, without their actions; ShellRuntime adds the actions, Help renders them before the browser reports commands. */
|
|
5
|
-
export const shellShortcuts = (t: ReturnType<typeof platformMessages.resolve>["t"]) => [
|
|
5
|
+
export const shellShortcuts = (t: ReturnType<typeof platformMessages.resolve>["t"], options: { panel?: boolean } = {}) => [
|
|
6
6
|
{ id: "cloud.search", title: t.openGlobalSearch, description: t.globalSearchDescription, icon: "ti ti-search", shortcut: "mod+k" },
|
|
7
7
|
{ id: "cloud.help", title: t.openShortcutHelp, description: t.shortcutHelpDescription, icon: "ti ti-help-circle", shortcut: "shift+/" },
|
|
8
|
+
...(options.panel
|
|
9
|
+
? [
|
|
10
|
+
{
|
|
11
|
+
id: "cloud.panel",
|
|
12
|
+
title: t.togglePanel,
|
|
13
|
+
description: t.togglePanelDescription,
|
|
14
|
+
icon: "ti ti-layout-sidebar-right",
|
|
15
|
+
shortcut: "mod+j",
|
|
16
|
+
},
|
|
17
|
+
]
|
|
18
|
+
: []),
|
|
8
19
|
];
|