@k2b/cloud 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- 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/define-app.ts +28 -2
- package/src/_internal/help-catalog.ts +123 -4
- package/src/_internal/help.ts +27 -6
- 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/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +30 -5
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/Avatar.tsx +20 -2
- package/src/account/ui.ts +1 -1
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/browser.ts +11 -0
- package/src/ai/chart-block.ts +44 -13
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/chart-block.tsx +2 -11
- package/src/ai/chat/messages.ts +10 -0
- package/src/ai/chat/turn-view.tsx +35 -3
- package/src/ai/chat/view-block.tsx +7 -2
- package/src/ai/chat/web-tools.tsx +23 -6
- package/src/ai/chat-task-contracts.ts +28 -3
- package/src/ai/chat-task-reasons-migrate.ts +68 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +460 -66
- package/src/ai/default-tool-contracts.ts +30 -13
- package/src/ai/fetch-file-tool.ts +37 -3
- package/src/ai/index.ts +12 -0
- package/src/ai/migrate.ts +46 -3
- package/src/ai/readable-html.ts +1057 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +27 -1
- package/src/ai/runtime.ts +12 -27
- package/src/ai/store.ts +5 -3
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-wording.ts +45 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +12 -1
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-outgoing-mail.ts +4 -2
- package/src/api/app-approval.ts +9 -2
- package/src/api/approvals.ts +1 -1
- package/src/api/help.ts +23 -4
- package/src/api/index.ts +2 -1
- package/src/api/mcp.ts +8 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/FileChooser.tsx +11 -3
- package/src/browser/app-approval.ts +7 -2
- package/src/browser/choose-files.browser-harness.tsx +4 -1
- package/src/browser/composer-catalog.ts +103 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +150 -0
- package/src/browser/file-chooser-messages.ts +4 -0
- package/src/browser/file-providers.ts +14 -1
- package/src/browser/panel-messages.ts +24 -0
- package/src/browser/panel-slot.ts +354 -0
- package/src/browser/panel.ts +28 -0
- package/src/browser/provider-folder.tsx +58 -5
- package/src/browser/resource-picker.tsx +17 -2
- package/src/cli/admin/workflows.ts +1 -0
- package/src/config/env.ts +14 -0
- package/src/contracts/app-approval.ts +49 -9
- package/src/contracts/app.ts +14 -0
- package/src/contracts/file-provider.ts +48 -1
- package/src/contracts/outgoing-mail.ts +8 -1
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/shared.ts +2 -0
- package/src/server/help.ts +136 -10
- package/src/server/services/access.ts +5 -1
- package/src/server/services/geo.ts +21 -4
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/allowed-network-fetch.ts +189 -0
- package/src/services/app-approval.ts +158 -38
- package/src/services/approvals/device.ts +2 -0
- package/src/services/help/index.ts +46 -5
- package/src/services/help/store.ts +26 -5
- package/src/services/help/types.ts +10 -2
- package/src/services/index.ts +22 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/outgoing-mail/dispatcher.ts +7 -2
- package/src/services/outgoing-mail/resolve-host.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +18 -3
- package/src/services/outgoing-mail/transport.ts +24 -13
- 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/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 +67 -43
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/help.ts +41 -0
- package/src/shared/index.ts +9 -1
- package/src/shared/markdown/extensions/guided-help.ts +208 -64
- package/src/shared/markdown/formula.ts +8 -8
- package/src/shared/markdown/index.ts +72 -11
- package/src/shared/redirect.ts +12 -0
- package/src/ssr/CloudShell.island.tsx +86 -9
- package/src/ssr/Layout.tsx +62 -11
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
- package/src/ssr/LayoutHelp.tsx +569 -410
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/PanelLayout.tsx +27 -0
- package/src/ssr/PanelRuntime.island.tsx +87 -0
- package/src/ssr/PanelSlot.tsx +108 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +72 -25
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +100 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +15 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +431 -0
- package/src/ssr/help-messages.ts +60 -38
- package/src/ssr/help.ts +3 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-help-article.ts +41 -0
- package/src/ssr/layout-help-navigation.ts +0 -12
- package/src/ssr/layout-help-registry.tsx +4 -11
- package/src/ssr/layout-help-search.ts +9 -12
- package/src/ssr/layout-help-start.ts +68 -0
- package/src/ssr/panel-slot.browser-harness.tsx +110 -0
- package/src/ssr/platform-messages.ts +34 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +11 -0
- package/src/ssr/shell-shortcuts.ts +19 -0
- package/src/styles/effects.css +689 -185
- package/src/styles/global.css +269 -55
- package/src/styles/panel.css +373 -0
- package/src/styles/resource-search.css +25 -0
- package/src/styles/tokens.css +23 -6
- package/src/styles/utilities-navigation.css +5 -21
- package/src/workflows/store/events.ts +24 -0
- package/src/workflows/store/index.ts +8 -0
- package/src/workflows/store/observability.ts +6 -1
- package/src/workflows/store/retention.ts +157 -0
|
@@ -1,30 +1,85 @@
|
|
|
1
|
-
import { useLocale } from "@k2b/ui";
|
|
2
|
-
import { createEffect } from "solid-js";
|
|
1
|
+
import { TabBar, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import { isServer, Portal } from "solid-js/web";
|
|
3
4
|
import { AppLaunchpadButton, setAppLaunchpadContext } from "./app-launchpad";
|
|
4
5
|
import GlobalSearchTrigger from "./GlobalSearchTrigger";
|
|
5
6
|
import LayoutBreadcrumbs from "./LayoutBreadcrumbs";
|
|
6
7
|
import LayoutHelpTrigger from "./LayoutHelpTrigger";
|
|
7
8
|
import LayoutPreferences from "./LayoutPreferences";
|
|
8
9
|
import type { LayoutBreadcrumb } from "./layout-runtime";
|
|
10
|
+
import PanelTrigger from "./PanelTrigger";
|
|
9
11
|
import ProfilePreferences from "./ProfilePreferences";
|
|
10
12
|
import { platformMessages } from "./platform-messages";
|
|
11
13
|
import RailApps from "./RailApps";
|
|
14
|
+
import { RAIL_PREFERENCES_EVENT, readRailContext } from "./rail-context";
|
|
15
|
+
import { phoneBarItems, projectRailNavigation } from "./rail-navigation";
|
|
12
16
|
import ShellFocusToggle from "./ShellFocusToggle";
|
|
13
17
|
import { useShellData } from "./shell-data";
|
|
14
18
|
|
|
15
19
|
type CloudShellProps =
|
|
16
|
-
| { slot: "rail"; currentPath: string }
|
|
17
|
-
| { slot: "header"; authenticated: boolean; title?: string; launchpad: boolean; breadcrumbs?: LayoutBreadcrumb[] };
|
|
20
|
+
| { slot: "rail"; currentPath: string; panelOpen?: boolean }
|
|
21
|
+
| { slot: "header"; authenticated: boolean; title?: string; launchpad: boolean; breadcrumbs?: LayoutBreadcrumb[]; panelOpen?: boolean };
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* The interactive part of the shell chrome. `LayoutRail` and `LayoutHeader`
|
|
21
25
|
* own the frame and the plain links around it as server markup.
|
|
22
26
|
*/
|
|
23
27
|
export default function CloudShell(props: CloudShellProps) {
|
|
24
|
-
return props.slot === "rail" ?
|
|
28
|
+
return props.slot === "rail" ? (
|
|
29
|
+
<RailNavigation currentPath={props.currentPath} panelOpen={props.panelOpen} />
|
|
30
|
+
) : (
|
|
31
|
+
<HeaderControls {...props} />
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The phone bar's live copy, mounted with the rail's island so the shell adds no island instance. It renders nothing
|
|
37
|
+
* while the server's bar is current, so mounting moves and refocuses nothing. When the saved rail preferences or the
|
|
38
|
+
* URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows, it renders the new bar into the
|
|
39
|
+
* bar's row, and CSS hides the server's.
|
|
40
|
+
*/
|
|
41
|
+
function BarNavigation(props: { currentPath: string }) {
|
|
42
|
+
if (isServer) return null;
|
|
43
|
+
const data = useShellData();
|
|
44
|
+
const locale = useLocale();
|
|
45
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
46
|
+
const initial = data?.rail;
|
|
47
|
+
if (!data || !initial) return null;
|
|
48
|
+
const [settings, setSettings] = createSignal(initial);
|
|
49
|
+
const [currentPath, setCurrentPath] = createSignal(props.currentPath);
|
|
50
|
+
const itemsFor = (rail: typeof initial, path: string) =>
|
|
51
|
+
phoneBarItems(projectRailNavigation(data.apps, rail, locale()), path, { label: t().home, path: data.home ?? "/" });
|
|
52
|
+
const served = JSON.stringify(itemsFor(initial, props.currentPath));
|
|
53
|
+
const items = createMemo(() => itemsFor(settings(), currentPath()));
|
|
54
|
+
onMount(() => {
|
|
55
|
+
const sync = () => setSettings(readRailContext()?.settings ?? initial);
|
|
56
|
+
const syncLocation = () => setCurrentPath(`${window.location.pathname}${window.location.search}${window.location.hash}`);
|
|
57
|
+
syncLocation();
|
|
58
|
+
window.addEventListener(RAIL_PREFERENCES_EVENT, sync);
|
|
59
|
+
window.addEventListener("popstate", syncLocation);
|
|
60
|
+
window.addEventListener("hashchange", syncLocation);
|
|
61
|
+
// In-page navigation (`@k2b/ssr/nav`) commits with pushState or replaceState, which only the Navigation API
|
|
62
|
+
// reports; where a browser lacks it, the bar follows at the next page load.
|
|
63
|
+
window.navigation?.addEventListener("currententrychange", syncLocation);
|
|
64
|
+
onCleanup(() => {
|
|
65
|
+
window.removeEventListener(RAIL_PREFERENCES_EVENT, sync);
|
|
66
|
+
window.removeEventListener("popstate", syncLocation);
|
|
67
|
+
window.removeEventListener("hashchange", syncLocation);
|
|
68
|
+
window.navigation?.removeEventListener("currententrychange", syncLocation);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
const row = document.querySelector<HTMLElement>(".cloud-tab-bar__row");
|
|
72
|
+
if (!row) return null;
|
|
73
|
+
return (
|
|
74
|
+
<Show when={JSON.stringify(items()) !== served}>
|
|
75
|
+
<Portal mount={row}>
|
|
76
|
+
<TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={items()} />
|
|
77
|
+
</Portal>
|
|
78
|
+
</Show>
|
|
79
|
+
);
|
|
25
80
|
}
|
|
26
81
|
|
|
27
|
-
function RailNavigation(props: { currentPath: string }) {
|
|
82
|
+
function RailNavigation(props: { currentPath: string; panelOpen?: boolean }) {
|
|
28
83
|
const data = useShellData();
|
|
29
84
|
const locale = useLocale();
|
|
30
85
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
@@ -36,7 +91,9 @@ function RailNavigation(props: { currentPath: string }) {
|
|
|
36
91
|
<ShellFocusToggle variant="rail" />
|
|
37
92
|
<RailApps apps={data.apps} settings={data.rail} currentPath={props.currentPath} />
|
|
38
93
|
<AppLaunchpadButton profile={profile} apps={data.apps} legalLinks={data.legalLinks} variant="rail" label={t().openApps} />
|
|
94
|
+
<BarNavigation currentPath={props.currentPath} />
|
|
39
95
|
<div class="mt-auto shrink-0 flex flex-col items-center gap-1">
|
|
96
|
+
{(data.panel?.providers.length ?? 0) > 1 && <PanelTrigger variant="rail" open={props.panelOpen} />}
|
|
40
97
|
<GlobalSearchTrigger variant="rail" />
|
|
41
98
|
<LayoutHelpTrigger variant="rail" />
|
|
42
99
|
<ProfilePreferences
|
|
@@ -51,7 +108,13 @@ function RailNavigation(props: { currentPath: string }) {
|
|
|
51
108
|
}
|
|
52
109
|
|
|
53
110
|
/** The header's title and controls, between the server-rendered Home and Sign in. */
|
|
54
|
-
function HeaderControls(props: {
|
|
111
|
+
function HeaderControls(props: {
|
|
112
|
+
authenticated: boolean;
|
|
113
|
+
title?: string;
|
|
114
|
+
launchpad: boolean;
|
|
115
|
+
breadcrumbs?: LayoutBreadcrumb[];
|
|
116
|
+
panelOpen?: boolean;
|
|
117
|
+
}) {
|
|
55
118
|
const data = useShellData();
|
|
56
119
|
const locale = useLocale();
|
|
57
120
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
@@ -65,13 +128,27 @@ function HeaderControls(props: { authenticated: boolean; title?: string; launchp
|
|
|
65
128
|
</div>
|
|
66
129
|
)}
|
|
67
130
|
<div class="flex min-w-0 items-center lg:hidden">
|
|
68
|
-
<span class="truncate font-semibold">{props.title}</span>
|
|
131
|
+
<span class="layout-header-title truncate font-semibold">{props.title}</span>
|
|
132
|
+
{props.launchpad && (
|
|
133
|
+
<AppLaunchpadButton
|
|
134
|
+
profile={
|
|
135
|
+
data.profile
|
|
136
|
+
? { name: data.profile.name, theme: data.theme, ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}) }
|
|
137
|
+
: undefined
|
|
138
|
+
}
|
|
139
|
+
apps={data.apps}
|
|
140
|
+
legalLinks={data.legalLinks}
|
|
141
|
+
variant="title"
|
|
142
|
+
label={props.title}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
69
145
|
</div>
|
|
70
146
|
</div>
|
|
71
147
|
<div class="flex shrink-0 items-center gap-1">
|
|
72
148
|
<div class="flex items-center gap-1 lg:hidden">
|
|
73
|
-
<
|
|
149
|
+
{(data.panel?.providers.length ?? 0) > 1 && <PanelTrigger variant="header" open={props.panelOpen} />}
|
|
74
150
|
{props.authenticated && <GlobalSearchTrigger variant="header" />}
|
|
151
|
+
<LayoutHelpTrigger variant="header" />
|
|
75
152
|
{props.launchpad && (
|
|
76
153
|
<AppLaunchpadButton
|
|
77
154
|
profile={
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { DateContext } from "@k2b/stdlib";
|
|
2
|
-
import { AppWorkspace, appWorkspaceLayoutStyle, LocaleProvider, NoticeCard, PageTrailProvider } from "@k2b/ui";
|
|
2
|
+
import { AppWorkspace, appWorkspaceLayoutStyle, keyboardPlatform, LocaleProvider, NoticeCard, PageTrailProvider } from "@k2b/ui";
|
|
3
3
|
import type { JSX } from "solid-js/jsx-runtime";
|
|
4
4
|
import { readAppWorkspaceLayoutCookie, resolveAppWorkspaceLayoutForSidebar } from "../_internal/app-workspace-state";
|
|
5
|
+
import { readPanelCookie } from "../_internal/panel-cookie";
|
|
5
6
|
import { readShellFocusCookie } from "../_internal/shell-focus";
|
|
6
7
|
import { resolveNavMatch } from "../contracts/app"; // ==========================
|
|
7
8
|
import { isPwaShellAvailable } from "../contracts/pwa";
|
|
@@ -9,18 +10,21 @@ import { defaultRailPreferences } from "../contracts/rail-preferences";
|
|
|
9
10
|
import { hasRole, type User } from "../contracts/shared";
|
|
10
11
|
import { getLocale } from "../server/locale";
|
|
11
12
|
import { getDateConfig } from "../server/time";
|
|
12
|
-
import { dates } from "../shared";
|
|
13
|
+
import { dates, resolveHomePath } from "../shared";
|
|
13
14
|
import { readThemePreferenceFromCookieHeader } from "../shared/theme";
|
|
14
15
|
import AnnouncementBanners from "./AnnouncementBanners";
|
|
15
16
|
import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
|
|
16
|
-
import { resolveRuntimeRoute, visibleNavigationApps } from "./app-navigation";
|
|
17
|
-
import { getLayoutHelp } from "./help";
|
|
17
|
+
import { resolveRuntimeRoute, visibleNavigationApps, visiblePanelProviders } from "./app-navigation";
|
|
18
|
+
import { getLayoutHelp, layoutHelpFailed } from "./help";
|
|
18
19
|
import LayoutFooter from "./LayoutFooter";
|
|
19
20
|
import LayoutHeader from "./LayoutHeader";
|
|
20
21
|
import LayoutRail, { type LayoutAppLink } from "./LayoutRail";
|
|
22
|
+
import LayoutTabBar from "./LayoutTabBar";
|
|
21
23
|
import type { LayoutContext } from "./layout-context";
|
|
24
|
+
import { HELP_VIEW_LIMIT } from "./layout-help-start";
|
|
22
25
|
import type { LayoutBreadcrumb } from "./layout-runtime";
|
|
23
26
|
import PageTrailRow from "./PageTrailRow";
|
|
27
|
+
import PanelSlot from "./PanelSlot";
|
|
24
28
|
import { derivePageTrail } from "./page-trail";
|
|
25
29
|
import { platformMessages } from "./platform-messages";
|
|
26
30
|
import { sortRailApps } from "./rail-navigation";
|
|
@@ -36,10 +40,16 @@ type LayoutProps = {
|
|
|
36
40
|
children: JSX.Element;
|
|
37
41
|
c: LayoutContext;
|
|
38
42
|
title?: string | Breadcrumb[];
|
|
43
|
+
/** Up to three ids of the current app's Help articles that fit this page; Help lists them first under "For this view". Unknown ids and articles the reader may not see are ignored. */
|
|
44
|
+
help?: readonly string[];
|
|
39
45
|
fullPage?: boolean /** Keep the shell viewport-bound and suppress its footer. */;
|
|
40
46
|
fullWidth?: boolean /** Delegate scrolling and clipping to the page's work surface. */;
|
|
47
|
+
/** Hide the panel slot on this page (sign-in, print, presentation, public pages). */
|
|
48
|
+
panel?: false;
|
|
41
49
|
focusMode?: boolean /** Render only the app canvas and content for dedicated editors or pop-out windows. */;
|
|
42
50
|
flushCanvas?: boolean /** Remove app-canvas spacing for edge-to-edge focus surfaces such as pop-out windows. */;
|
|
51
|
+
/** Final localized app-specific export or printing guidance, shown when no data-print-region exists. */
|
|
52
|
+
printHint?: string;
|
|
43
53
|
workspaceSidebarCollapsible?: boolean /** Resolve persisted sidebar geometry for the page's sidebar policy during SSR. */;
|
|
44
54
|
}; // ==========================
|
|
45
55
|
// Helpers
|
|
@@ -150,6 +160,12 @@ export default function Layout(props: LayoutProps) {
|
|
|
150
160
|
const lang = getLocale(c);
|
|
151
161
|
const t = platformMessages.resolve([lang]).t;
|
|
152
162
|
const user = c.get("user");
|
|
163
|
+
const providers = user && props.panel !== false ? visiblePanelProviders(runtime.apps, user, lang) : [];
|
|
164
|
+
const panelState = readPanelCookie(
|
|
165
|
+
providers.length ? cookie : undefined,
|
|
166
|
+
providers.map((provider) => provider.id),
|
|
167
|
+
);
|
|
168
|
+
const panel = providers.length ? { providers, provider: panelState.provider ?? providers[0]?.id } : undefined;
|
|
153
169
|
const pathname = new URL(c.req.raw.url).pathname;
|
|
154
170
|
const currentRoute = resolveRuntimeRoute(runtime.apps, pathname);
|
|
155
171
|
const currentApp = resolveCurrentApp(runtime.apps, pathname);
|
|
@@ -202,14 +218,29 @@ export default function Layout(props: LayoutProps) {
|
|
|
202
218
|
// keeps short and long pages at the same width where scrollbars take space.
|
|
203
219
|
const mainLayoutClass = fullPage || fullWidth ? "flex flex-col" : "md:overflow-auto md:[scrollbar-gutter:stable]";
|
|
204
220
|
const canvasStyle =
|
|
205
|
-
[
|
|
221
|
+
[
|
|
222
|
+
appAppearanceStyle(currentApp?.appearance),
|
|
223
|
+
appWorkspaceLayoutStyle(workspaceLayout),
|
|
224
|
+
panelState.widthRem !== undefined ? `--cloud-panel-width-set:${panelState.widthRem}rem` : "",
|
|
225
|
+
focusMode && flushCanvas ? "padding:0" : "",
|
|
226
|
+
]
|
|
206
227
|
.filter(Boolean)
|
|
207
228
|
.join(";") || undefined;
|
|
208
229
|
const appLabel = currentApp?.name ?? (pathname.startsWith("/admin") ? t.admin : undefined);
|
|
230
|
+
const helpIds = new Set(registeredHelp?.documents.map((topic) => topic.id));
|
|
231
|
+
const viewHelp = [...new Set(props.help)].filter((id) => helpIds.has(id)).slice(0, HELP_VIEW_LIMIT);
|
|
209
232
|
const shellData: ShellData = {
|
|
233
|
+
...(panel ? { panel } : {}),
|
|
234
|
+
platform: keyboardPlatform(c.req.raw.headers.get("User-Agent") ?? ""),
|
|
235
|
+
...(viewHelp.length ? { help: viewHelp } : {}),
|
|
236
|
+
...(layoutHelpFailed(c) ? { helpFailed: true as const } : {}),
|
|
210
237
|
apps: allApps,
|
|
211
238
|
...(user
|
|
212
|
-
? {
|
|
239
|
+
? {
|
|
240
|
+
rail: c.get("railPreferences") ?? defaultRailPreferences(),
|
|
241
|
+
home: resolveHomePath(settings?.app?.home_path),
|
|
242
|
+
profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref },
|
|
243
|
+
}
|
|
213
244
|
: {}),
|
|
214
245
|
legalLinks,
|
|
215
246
|
theme,
|
|
@@ -221,20 +252,31 @@ export default function Layout(props: LayoutProps) {
|
|
|
221
252
|
return (
|
|
222
253
|
<LocaleProvider locale={lang}>
|
|
223
254
|
<div
|
|
224
|
-
class="cloud-app-canvas flex h-dvh w-full overflow-hidden"
|
|
255
|
+
class="cloud-app-canvas relative flex h-dvh w-full overflow-hidden"
|
|
225
256
|
style={canvasStyle}
|
|
257
|
+
data-panel-transient=""
|
|
226
258
|
data-app-id={currentApp?.id}
|
|
227
259
|
data-app-base={currentRoute?.prefix}
|
|
228
260
|
data-page-trail-title-only=""
|
|
229
261
|
data-layout-full-page="true"
|
|
230
262
|
data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
|
|
231
263
|
>
|
|
232
|
-
<ShellRuntime pageTrail help={runtimeHelp} />
|
|
264
|
+
<ShellRuntime pageTrail help={runtimeHelp} panel={panel ? true : undefined} />
|
|
233
265
|
<PageTrailProvider trail={pageTrail}>
|
|
234
|
-
<main class="min-h-0 min-w-0 flex-1">
|
|
266
|
+
<main class="min-h-0 min-w-0 flex-1" data-cloud-app-area="">
|
|
235
267
|
<AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
|
|
236
268
|
</main>
|
|
237
269
|
</PageTrailProvider>
|
|
270
|
+
{/* Focus pages carry shell data only for their panel providers. */}
|
|
271
|
+
{panel && (
|
|
272
|
+
<ShellDataProvider data={{ panel, platform: shellData.platform, theme, apps: [], legalLinks: [], searchLinks: {} }}>
|
|
273
|
+
<PanelSlot panel={panel} state={{ ...panelState, open: false }} platform={shellData.platform} />
|
|
274
|
+
</ShellDataProvider>
|
|
275
|
+
)}
|
|
276
|
+
<div data-print-fallback="">
|
|
277
|
+
<p>{t.printFallbackTitle}</p>
|
|
278
|
+
<p>{props.printHint ?? t.printFallbackHint}</p>
|
|
279
|
+
</div>
|
|
238
280
|
</div>
|
|
239
281
|
</LocaleProvider>
|
|
240
282
|
);
|
|
@@ -247,6 +289,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
247
289
|
<div
|
|
248
290
|
class={`cloud-app-canvas relative flex w-full ${fullPage ? "h-dvh overflow-hidden" : "min-h-dvh md:h-dvh md:overflow-hidden"}`}
|
|
249
291
|
style={canvasStyle}
|
|
292
|
+
data-panel={panelState.open ? "open" : undefined}
|
|
250
293
|
data-app-id={currentApp?.id}
|
|
251
294
|
data-app-base={currentRoute?.prefix}
|
|
252
295
|
data-page-trail-title-only={titleOnly ? "" : undefined}
|
|
@@ -256,6 +299,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
256
299
|
data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
|
|
257
300
|
>
|
|
258
301
|
<ShellRuntime
|
|
302
|
+
panel={panel ? true : undefined}
|
|
259
303
|
workspace={{ appId: currentApp?.id }}
|
|
260
304
|
pageTrail
|
|
261
305
|
focus={showRail}
|
|
@@ -270,11 +314,12 @@ export default function Layout(props: LayoutProps) {
|
|
|
270
314
|
banners={user && announcements?.banners.length ? true : undefined}
|
|
271
315
|
/>
|
|
272
316
|
<ShellDataProvider data={shellData}>
|
|
273
|
-
{showRail && <LayoutRail currentPath={requestPath(c)} />}
|
|
317
|
+
{showRail && <LayoutRail currentPath={requestPath(c)} panelOpen={panel ? panelState.open : undefined} />}
|
|
274
318
|
{showRail && <ShellFocusToggle variant="float" />}
|
|
275
319
|
{showRail && <div class="cloud-shell-reveal" data-cloud-shell-reveal="" aria-hidden="true" />}
|
|
276
|
-
<div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col">
|
|
320
|
+
<div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col" data-cloud-app-area="">
|
|
277
321
|
<LayoutHeader
|
|
322
|
+
panelOpen={panel ? panelState.open : undefined}
|
|
278
323
|
authenticated={showRail}
|
|
279
324
|
title={appLabel ?? breadcrumbs.at(-1)?.title}
|
|
280
325
|
launchpad={!!(user || appLabel)}
|
|
@@ -291,7 +336,13 @@ export default function Layout(props: LayoutProps) {
|
|
|
291
336
|
</PageTrailProvider>
|
|
292
337
|
{!fullPage && !showRail && <LayoutFooter appName={settings?.app?.copyright || appName} legalLinks={legalLinks} />}
|
|
293
338
|
</div>
|
|
339
|
+
{panel && <PanelSlot panel={panel} state={panelState} platform={shellData.platform} />}
|
|
340
|
+
{showRail && <LayoutTabBar currentPath={requestPath(c)} />}
|
|
294
341
|
</ShellDataProvider>
|
|
342
|
+
<div data-print-fallback="">
|
|
343
|
+
<p>{t.printFallbackTitle}</p>
|
|
344
|
+
<p>{props.printHint ?? t.printFallbackHint}</p>
|
|
345
|
+
</div>
|
|
295
346
|
</div>
|
|
296
347
|
</LocaleProvider>
|
|
297
348
|
);
|
package/src/ssr/LayoutHeader.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { platformMessages } from "./platform-messages";
|
|
|
5
5
|
|
|
6
6
|
type LayoutHeaderProps = {
|
|
7
7
|
authenticated: boolean;
|
|
8
|
+
panelOpen?: boolean;
|
|
8
9
|
title?: string;
|
|
9
10
|
launchpad: boolean;
|
|
10
11
|
breadcrumbs?: LayoutBreadcrumb[];
|
|
@@ -14,10 +15,12 @@ type LayoutHeaderProps = {
|
|
|
14
15
|
* Phones (below `md`) always get this header. From `md` it remains only on
|
|
15
16
|
* anonymous pages; signed-in people reach Home, search, help, the app grid,
|
|
16
17
|
* and their profile through the rail, so the app area starts at the top edge.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* On a signed-in phone it is the title row on the app gradient: Home moves to
|
|
19
|
+
* the bottom bar's Start, so the row holds the title, search, help, and the
|
|
20
|
+
* app menu. Anonymous pages have no rail: up to `lg` their header keeps Home,
|
|
21
|
+
* the app name, help, search, and the app grid, and from `lg` it shows the
|
|
22
|
+
* breadcrumbs. Home and Sign in stay server markup, so focus on them survives
|
|
23
|
+
* the shell's mount; the shell renders the title and the controls between them.
|
|
21
24
|
*/
|
|
22
25
|
export default function LayoutHeader(props: LayoutHeaderProps) {
|
|
23
26
|
const locale = useLocale();
|
|
@@ -29,13 +32,16 @@ export default function LayoutHeader(props: LayoutHeaderProps) {
|
|
|
29
32
|
>
|
|
30
33
|
{/* A long title must not squeeze the button; `shrink-0` sits on a wrapper because the button's own
|
|
31
34
|
styles would win over a utility on the link. */}
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
{!props.authenticated && (
|
|
36
|
+
<div class="flex shrink-0">
|
|
37
|
+
<IconButtonLink href="/" label={t().home}>
|
|
38
|
+
<img src="/branding/logo" alt="" class="h-6 w-6" />
|
|
39
|
+
</IconButtonLink>
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
37
42
|
<CloudShell
|
|
38
43
|
slot="header"
|
|
44
|
+
panelOpen={props.panelOpen}
|
|
39
45
|
authenticated={props.authenticated}
|
|
40
46
|
title={props.title}
|
|
41
47
|
launchpad={props.launchpad}
|
|
@@ -1,43 +1,146 @@
|
|
|
1
|
-
import { LocaleProvider } from "@k2b/ui";
|
|
1
|
+
import { AppWorkspace, LocaleProvider } from "@k2b/ui";
|
|
2
2
|
import { render } from "solid-js/web";
|
|
3
|
+
import { registerContextAwareCommand } from "../browser/command-bridge";
|
|
3
4
|
import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
|
|
4
|
-
import { LayoutHelpPage } from "./LayoutHelp";
|
|
5
|
+
import { LayoutHelpPage, openLayoutHelpDialog } from "./LayoutHelp";
|
|
6
|
+
import { registry } from "./layout-help-registry";
|
|
5
7
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const params = new URL(location.href).searchParams;
|
|
9
|
+
const fixture = (id: string, title: string, order: number, kind?: HelpDocumentManifest["kind"], icon?: string): HelpDocumentManifest => ({
|
|
10
|
+
id,
|
|
11
|
+
title,
|
|
9
12
|
description: "Help browser fixture",
|
|
10
|
-
order
|
|
11
|
-
|
|
13
|
+
order,
|
|
14
|
+
...(kind ? { kind } : {}),
|
|
15
|
+
...(icon ? { icon } : {}),
|
|
16
|
+
url: `/api/help/${id}`,
|
|
12
17
|
searchUrl: "/api/help/search",
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
(
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
});
|
|
19
|
+
// `?start`: an app with every kind and more tasks than the start shows as tiles.
|
|
20
|
+
const startDocuments: HelpDocumentManifest[] = [
|
|
21
|
+
fixture("share-space", "Share a Space", 10, "task", "ti ti-share"),
|
|
22
|
+
fixture("claim-task", "Claim a task", 20, "task", "ti ti-hand-grab"),
|
|
23
|
+
fixture("statuses", "Create and order statuses", 30, "task", "ti ti-arrows-sort"),
|
|
24
|
+
fixture("calendar-feed", "See dates in your own calendar", 40, "task", "ti ti-calendar-share"),
|
|
25
|
+
fixture("templates", "Start from a template", 50, undefined, "ti ti-template"),
|
|
26
|
+
fixture("filters", "Filter and find items", 60, "task", "ti ti-filter"),
|
|
27
|
+
fixture("handoffs", "Hand work over", 70, "task", "ti ti-arrows-exchange"),
|
|
28
|
+
fixture("archive", "Archive a Space", 80, "task", "ti ti-archive"),
|
|
29
|
+
fixture("import", "Import tasks from a spreadsheet with a very long title that wraps", 90, "task", "ti ti-table-import"),
|
|
30
|
+
fixture("recurring", "Repeat a task", 100, "task", "ti ti-repeat"),
|
|
31
|
+
fixture("start", "Start with Spaces", 5, "start", "ti ti-rocket"),
|
|
32
|
+
fixture("access-levels", "Access levels", 110, "reference", "ti ti-key"),
|
|
33
|
+
fixture("troubleshoot", "Fix problems in Spaces", 120, "troubleshoot", "ti ti-lifebuoy"),
|
|
34
|
+
];
|
|
35
|
+
const documents: HelpDocumentManifest[] = params.has("start")
|
|
36
|
+
? startDocuments
|
|
37
|
+
: Array.from({ length: 8 }, (_, index) => fixture(`topic-${index}`, `Topic ${index}`, index));
|
|
38
|
+
// `?commands`: what the shell and an app register on a real page.
|
|
39
|
+
if (params.has("commands")) {
|
|
40
|
+
for (const command of [
|
|
41
|
+
{ id: "cloud.shell.focus", title: "Hide sidebar", description: "", shortcut: "mod+\\" },
|
|
42
|
+
{ id: "cloud.search", title: "Open global search", description: "", shortcut: "mod+k" },
|
|
43
|
+
{ id: "cloud.help", title: "Open shortcut help", description: "", shortcut: "shift+/" },
|
|
44
|
+
{ id: "spaces.new", title: "New task", description: "", shortcut: "n" },
|
|
45
|
+
{ id: "spaces.filter", title: "Filter tasks", description: "", shortcut: "f" },
|
|
46
|
+
{ id: "spaces.complete", title: "Complete selected", description: "", shortcut: "x", scope: "selection" as const },
|
|
47
|
+
])
|
|
48
|
+
registerContextAwareCommand({ ...command, action: () => {} });
|
|
49
|
+
}
|
|
50
|
+
const lead = "<p>Share a Space so that other people can see and change its tasks.</p>";
|
|
51
|
+
const longHtml =
|
|
52
|
+
lead +
|
|
53
|
+
Array.from(
|
|
54
|
+
{ length: 8 },
|
|
55
|
+
(_, index) =>
|
|
56
|
+
`<h2 id="section-${index}" class="help-section-title">Section ${index}</h2>${Array.from({ length: 18 }, () => "<p>Read this guidance to finish the task and understand how the app works.</p>").join("")}`,
|
|
57
|
+
).join("");
|
|
58
|
+
// A short task article: the short answer, one section, four steps.
|
|
59
|
+
const taskHtml = `${lead}<h2 id="invite-people" class="help-section-title">Invite people</h2><div class="help-steps"><ol class="k2b-steps"><li>Open <span class="k2b-label-chip">Space settings</span>.</li><li>Choose <span class="k2b-label-chip">Add person</span>.</li><li>Enter a name or a group.</li><li>Choose the access level.</li></ol></div><p>Spaces saves the change at once.</p><div class="help-compare"><ul><li><strong>Add a person</strong> Gives one named person access to every task in this Space.</li><li><strong>Add a group</strong> Gives everyone in the group access, including people who join later.</li></ul></div>`;
|
|
60
|
+
const link = (id: string, title: string, appId = "spaces", appName = "Spaces") => ({
|
|
61
|
+
appId,
|
|
62
|
+
appName,
|
|
63
|
+
accent: appId === "spaces" ? "#16a34a" : "#2563eb",
|
|
64
|
+
documentId: id,
|
|
65
|
+
title,
|
|
66
|
+
description: "Help browser fixture",
|
|
67
|
+
url: `/help/apps/${appId}/${id}`,
|
|
68
|
+
});
|
|
19
69
|
window.fetch = Object.assign(
|
|
20
70
|
async (input: URL | RequestInfo) => {
|
|
21
71
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
22
72
|
const path = String(input);
|
|
23
|
-
if (path.includes("/search"))
|
|
73
|
+
if (path.includes("/search")) {
|
|
74
|
+
const query = new URL(path, location.href).searchParams.get("q") ?? "";
|
|
75
|
+
const ids = params.has("start")
|
|
76
|
+
? documents.filter((document) => document.title.toLocaleLowerCase().includes(query)).map((document) => document.id)
|
|
77
|
+
: ["topic-0", "topic-1"];
|
|
78
|
+
return Response.json({ locale: "en", ids });
|
|
79
|
+
}
|
|
24
80
|
const id = path.split("/").at(-1) ?? "topic-0";
|
|
25
|
-
const payload: HelpDocumentPayload = {
|
|
81
|
+
const payload: HelpDocumentPayload = {
|
|
82
|
+
id,
|
|
83
|
+
locale: "en",
|
|
84
|
+
title: id,
|
|
85
|
+
markdown: "Help",
|
|
86
|
+
html: id === "topic-1" ? taskHtml : longHtml,
|
|
87
|
+
kind: "task",
|
|
88
|
+
needs: "manage",
|
|
89
|
+
next: link("topic-2", "Topic 2"),
|
|
90
|
+
related: [link("topic-3", "Topic 3"), link("mail-share", "Share a mailbox", "mail", "Mail")],
|
|
91
|
+
};
|
|
26
92
|
return Response.json(payload);
|
|
27
93
|
},
|
|
28
94
|
{ preconnect: () => {} },
|
|
29
95
|
);
|
|
30
96
|
const main = document.querySelector<HTMLElement>(".layout-content-main");
|
|
31
97
|
if (!main) throw new Error("Missing Layout main");
|
|
32
|
-
const embedded =
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
const embedded = params.has("embedded");
|
|
99
|
+
const referenceHost = params.get("host");
|
|
100
|
+
if (referenceHost === "grids" || referenceHost === "pulse") {
|
|
101
|
+
// The Grids and Pulse query reference windows: one article beside the reference sidebar, without the Cloud shell.
|
|
102
|
+
const host = document.createElement("div");
|
|
103
|
+
host.className = "h-dvh";
|
|
104
|
+
document.body.replaceChildren(host);
|
|
105
|
+
const reader = () => <LayoutHelpPage documents={documents.slice(0, 1)} initialTopic="topic-0" includeShortcuts={false} embedded />;
|
|
106
|
+
render(
|
|
107
|
+
() => (
|
|
108
|
+
<LocaleProvider locale="en">
|
|
109
|
+
<AppWorkspace>
|
|
110
|
+
<AppWorkspace.Sidebar>
|
|
111
|
+
<AppWorkspace.SidebarDesktop>
|
|
112
|
+
<AppWorkspace.SidebarBody>Reference</AppWorkspace.SidebarBody>
|
|
113
|
+
</AppWorkspace.SidebarDesktop>
|
|
114
|
+
</AppWorkspace.Sidebar>
|
|
115
|
+
<AppWorkspace.Content>
|
|
116
|
+
{referenceHost === "grids" ? (
|
|
117
|
+
<AppWorkspace.Main class="p-[var(--ui-space-shell)]" scroll={false}>
|
|
118
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-auto">{reader()}</div>
|
|
119
|
+
</AppWorkspace.Main>
|
|
120
|
+
) : (
|
|
121
|
+
<AppWorkspace.Main class="overflow-y-auto">{reader()}</AppWorkspace.Main>
|
|
122
|
+
)}
|
|
123
|
+
</AppWorkspace.Content>
|
|
124
|
+
</AppWorkspace>
|
|
125
|
+
</LocaleProvider>
|
|
126
|
+
),
|
|
127
|
+
host,
|
|
128
|
+
);
|
|
129
|
+
} else if (params.has("modal")) {
|
|
130
|
+
// The dialog reads the topics the page registered, as Layout.HelpDocuments does.
|
|
131
|
+
for (const manifest of documents) registry()?.set(manifest.id, manifest);
|
|
132
|
+
window.__cloudLayoutHelpPageBase = "/app/spaces/help";
|
|
133
|
+
openLayoutHelpDialog();
|
|
134
|
+
} else {
|
|
135
|
+
const host = document.createElement("div");
|
|
136
|
+
if (embedded) host.style.cssText = "width:360px;height:700px";
|
|
137
|
+
main.append(host);
|
|
138
|
+
render(
|
|
139
|
+
() => (
|
|
140
|
+
<LocaleProvider locale="en">
|
|
141
|
+
<LayoutHelpPage documents={documents} embedded={embedded} pageBase="/app/spaces/help" />
|
|
142
|
+
</LocaleProvider>
|
|
143
|
+
),
|
|
144
|
+
host,
|
|
145
|
+
);
|
|
146
|
+
}
|