@k2b/cloud 0.32.0 → 0.33.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 +5 -5
- package/src/_internal/define-app.ts +17 -8
- package/src/_internal/page-responses.ts +5 -1
- package/src/ai/code-source-contracts.ts +17 -2
- package/src/ai/code-source-tools.ts +1 -0
- package/src/ai/default-tools.ts +4 -4
- package/src/ai/memories.ts +48 -1
- package/src/ai/memory-learning.ts +17 -6
- package/src/ai/memory-tool.ts +1 -0
- package/src/ai/migrate.ts +8 -0
- package/src/ai/provider.ts +4 -3
- package/src/browser/navigation-search.ts +4 -1
- package/src/services/outbox.ts +13 -6
- package/src/shared/ai-platform-prompt.ts +3 -2
- package/src/shared/theme.ts +41 -20
- package/src/ssr/AnnouncementBanners.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +91 -0
- package/src/ssr/GlobalSearchTrigger.tsx +51 -0
- package/src/ssr/Layout.tsx +56 -85
- package/src/ssr/LayoutHeader.tsx +31 -77
- package/src/ssr/LayoutHelp.tsx +2 -57
- package/src/ssr/LayoutHelpTrigger.tsx +51 -0
- package/src/ssr/{LayoutPreferences.island.tsx → LayoutPreferences.tsx} +2 -2
- package/src/ssr/LayoutRail.tsx +15 -69
- package/src/ssr/MinimalLayout.tsx +2 -3
- package/src/ssr/MinimalLayoutPreferences.island.tsx +12 -15
- package/src/ssr/MobileNavigation.tsx +13 -8
- package/src/ssr/MobileProfileActions.tsx +10 -7
- package/src/ssr/PageTrailRow.tsx +14 -0
- package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +12 -7
- package/src/ssr/PwaLayout.tsx +0 -2
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/ShellRuntime.island.tsx +170 -0
- package/src/ssr/TimezoneCookie.island.tsx +3 -19
- package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
- package/src/ssr/announcements-dialog.tsx +66 -0
- package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
- package/src/ssr/busy-action.ts +19 -0
- package/src/ssr/islands/index.ts +9 -6
- package/src/ssr/launchpad-surface.tsx +15 -0
- package/src/ssr/layout-help-registry.tsx +60 -0
- package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
- package/src/ssr/preference-controller.ts +30 -21
- package/src/ssr/profile-actions.ts +26 -15
- package/src/ssr/profile-preferences-messages.ts +8 -4
- package/src/ssr/rail-context.ts +8 -12
- package/src/ssr/shell-data.tsx +48 -0
- package/src/ssr/timezone-cookie.ts +22 -0
- package/src/ssr/workspace-navigation.ts +25 -2
- package/src/styles/global.css +17 -5
- package/src/styles/utilities-navigation.css +13 -0
- package/src/workflows/language/compiler.ts +1 -1
- package/src/ssr/AppWorkspaceController.island.tsx +0 -21
- package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
- package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
- package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
- package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
- package/src/ssr/PageTrailRow.island.tsx +0 -20
- package/src/ssr/PageTrailRuntime.island.tsx +0 -26
- package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
- /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
- /package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +0 -0
|
@@ -3,7 +3,7 @@ import { IconButton, ScrollArea, TextInput, useLocale } from "@k2b/ui";
|
|
|
3
3
|
import { createSignal, For, type JSX, Show } from "solid-js";
|
|
4
4
|
import { Dynamic } from "solid-js/web";
|
|
5
5
|
import { importOnDemand } from "../browser/reload";
|
|
6
|
-
import type {
|
|
6
|
+
import type { CloudThemePreference } from "../shared/theme";
|
|
7
7
|
import { AppBadge, useAppBadgeDescription } from "./AppBadge";
|
|
8
8
|
import { MobileProfileActions } from "./MobileProfileActions";
|
|
9
9
|
import { platformMessages } from "./platform-messages";
|
|
@@ -29,7 +29,7 @@ export type AppLaunchpadLegalLink = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export type AppLaunchpadContext = {
|
|
32
|
-
profile?: { name: string; theme:
|
|
32
|
+
profile?: { name: string; theme: CloudThemePreference; appHref?: string };
|
|
33
33
|
apps: AppLaunchpadApp[];
|
|
34
34
|
legalLinks: AppLaunchpadLegalLink[];
|
|
35
35
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useLocale } from "@k2b/ui";
|
|
2
|
+
import { createEffect } from "solid-js";
|
|
3
|
+
import { AppLaunchpadButton, setAppLaunchpadContext } from "./app-launchpad";
|
|
4
|
+
import GlobalSearchTrigger from "./GlobalSearchTrigger";
|
|
5
|
+
import LayoutBreadcrumbs from "./LayoutBreadcrumbs";
|
|
6
|
+
import LayoutHelpTrigger from "./LayoutHelpTrigger";
|
|
7
|
+
import LayoutPreferences from "./LayoutPreferences";
|
|
8
|
+
import type { LayoutBreadcrumb } from "./layout-runtime";
|
|
9
|
+
import ProfilePreferences from "./ProfilePreferences";
|
|
10
|
+
import { platformMessages } from "./platform-messages";
|
|
11
|
+
import RailApps from "./RailApps";
|
|
12
|
+
import { useShellData } from "./shell-data";
|
|
13
|
+
|
|
14
|
+
type CloudShellProps =
|
|
15
|
+
| { slot: "rail"; currentPath: string }
|
|
16
|
+
| { slot: "header"; authenticated: boolean; title?: string; launchpad: boolean; breadcrumbs?: LayoutBreadcrumb[] };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The interactive part of the shell chrome. `LayoutRail` and `LayoutHeader`
|
|
20
|
+
* own the frame and the plain links around it as server markup.
|
|
21
|
+
*/
|
|
22
|
+
export default function CloudShell(props: CloudShellProps) {
|
|
23
|
+
return props.slot === "rail" ? <RailNavigation currentPath={props.currentPath} /> : <HeaderControls {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function RailNavigation(props: { currentPath: string }) {
|
|
27
|
+
const data = useShellData();
|
|
28
|
+
const locale = useLocale();
|
|
29
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
30
|
+
if (!data?.rail || !data.profile) return null;
|
|
31
|
+
const profile = { name: data.profile.name, theme: data.theme, ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}) };
|
|
32
|
+
createEffect(() => setAppLaunchpadContext(data.apps, data.legalLinks, profile));
|
|
33
|
+
return (
|
|
34
|
+
<nav class="layout-rail-navigation flex min-h-0 flex-1 flex-col items-center gap-1" aria-label={t().apps}>
|
|
35
|
+
<RailApps apps={data.apps} settings={data.rail} currentPath={props.currentPath} />
|
|
36
|
+
<AppLaunchpadButton profile={profile} apps={data.apps} legalLinks={data.legalLinks} variant="rail" label={t().openApps} />
|
|
37
|
+
<div class="mt-auto shrink-0 flex flex-col items-center gap-1">
|
|
38
|
+
<GlobalSearchTrigger variant="rail" />
|
|
39
|
+
<LayoutHelpTrigger variant="rail" />
|
|
40
|
+
<ProfilePreferences
|
|
41
|
+
appHref={data.profile.appHref}
|
|
42
|
+
avatarSrc={data.profile.avatarSrc}
|
|
43
|
+
initialTheme={data.theme}
|
|
44
|
+
name={data.profile.name}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</nav>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The header's title and controls, between the server-rendered Home and Sign in. */
|
|
52
|
+
function HeaderControls(props: { authenticated: boolean; title?: string; launchpad: boolean; breadcrumbs?: LayoutBreadcrumb[] }) {
|
|
53
|
+
const data = useShellData();
|
|
54
|
+
const locale = useLocale();
|
|
55
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
56
|
+
if (!data) return null;
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<div class="ml-2 flex min-w-0 flex-1 items-center">
|
|
60
|
+
{!props.authenticated && (
|
|
61
|
+
<div class="hidden min-w-0 items-center lg:flex">
|
|
62
|
+
<LayoutBreadcrumbs breadcrumbs={props.breadcrumbs ?? []} />
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
<div class="flex min-w-0 items-center lg:hidden">
|
|
66
|
+
<span class="truncate font-semibold">{props.title}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
70
|
+
<div class="flex items-center gap-1 lg:hidden">
|
|
71
|
+
<LayoutHelpTrigger variant="header" />
|
|
72
|
+
{props.authenticated && <GlobalSearchTrigger variant="header" />}
|
|
73
|
+
{props.launchpad && (
|
|
74
|
+
<AppLaunchpadButton
|
|
75
|
+
profile={
|
|
76
|
+
data.profile
|
|
77
|
+
? { name: data.profile.name, theme: data.theme, ...(data.profile.appHref ? { appHref: data.profile.appHref } : {}) }
|
|
78
|
+
: undefined
|
|
79
|
+
}
|
|
80
|
+
apps={data.apps}
|
|
81
|
+
legalLinks={data.legalLinks}
|
|
82
|
+
variant="header"
|
|
83
|
+
label={t().openApps}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
{!props.authenticated && <LayoutPreferences initialTheme={data.theme} position="bottom-left" />}
|
|
88
|
+
</div>
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IconButton, Tooltip, useLocale } from "@k2b/ui";
|
|
2
|
+
import { importOnDemand } from "../browser/reload";
|
|
3
|
+
import { openGlobalSearch } from "../browser/search";
|
|
4
|
+
import { createBusyAction } from "./busy-action";
|
|
5
|
+
import { platformMessages } from "./platform-messages";
|
|
6
|
+
|
|
7
|
+
type GlobalSearchTriggerProps = {
|
|
8
|
+
variant: "header" | "rail";
|
|
9
|
+
class?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Opens the spotlight-style global search dialog from nav/header trigger points. */
|
|
13
|
+
export default function GlobalSearchTrigger(props: GlobalSearchTriggerProps) {
|
|
14
|
+
const locale = useLocale();
|
|
15
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
16
|
+
|
|
17
|
+
const action = createBusyAction();
|
|
18
|
+
const open = () =>
|
|
19
|
+
action.run(
|
|
20
|
+
() => importOnDemand(() => import("./GlobalSearchDialog")),
|
|
21
|
+
() => openGlobalSearch(),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (props.variant === "rail")
|
|
25
|
+
return (
|
|
26
|
+
<Tooltip.Trigger
|
|
27
|
+
type="button"
|
|
28
|
+
class={`rail-item text-blue-500 hover:bg-blue-500/10 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/15 dark:hover:text-blue-300 ${props.class ?? ""}`}
|
|
29
|
+
onClick={open}
|
|
30
|
+
aria-busy={action.busy() ? "true" : undefined}
|
|
31
|
+
aria-label={t().openGlobalSearch}
|
|
32
|
+
placement="right"
|
|
33
|
+
delay={0}
|
|
34
|
+
content={t().searchShortcut}
|
|
35
|
+
>
|
|
36
|
+
<i class={`${action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-search"} text-base`} />
|
|
37
|
+
</Tooltip.Trigger>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<IconButton
|
|
42
|
+
class={props.class}
|
|
43
|
+
onClick={open}
|
|
44
|
+
aria-busy={action.busy() ? "true" : undefined}
|
|
45
|
+
label={t().openGlobalSearch}
|
|
46
|
+
title={t().searchShortcut}
|
|
47
|
+
>
|
|
48
|
+
<i class={`${action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-search"} text-base`} />
|
|
49
|
+
</IconButton>
|
|
50
|
+
);
|
|
51
|
+
}
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -2,7 +2,6 @@ import type { DateContext } from "@k2b/stdlib";
|
|
|
2
2
|
import { AppWorkspace, appWorkspaceLayoutStyle, 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 { navigationSearchItems } from "../browser/navigation-search";
|
|
6
5
|
import { resolveNavMatch } from "../contracts/app"; // ==========================
|
|
7
6
|
import { isPwaShellAvailable } from "../contracts/pwa";
|
|
8
7
|
import { defaultRailPreferences } from "../contracts/rail-preferences";
|
|
@@ -10,13 +9,10 @@ import { hasRole, type User } from "../contracts/shared";
|
|
|
10
9
|
import { getLocale } from "../server/locale";
|
|
11
10
|
import { getDateConfig } from "../server/time";
|
|
12
11
|
import { dates } from "../shared";
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import AppWorkspaceController from "./AppWorkspaceController.island";
|
|
12
|
+
import { readThemePreferenceFromCookieHeader } from "../shared/theme";
|
|
13
|
+
import AnnouncementBanners from "./AnnouncementBanners";
|
|
16
14
|
import { appAppearanceStyle } from "./app-appearance";
|
|
17
15
|
import { resolveRuntimeRoute, visibleNavigationApps } from "./app-navigation";
|
|
18
|
-
import BrowserPushRegistration from "./BrowserPushRegistration.island";
|
|
19
|
-
import GlobalAnnouncements from "./GlobalAnnouncements.island";
|
|
20
16
|
import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
21
17
|
import { getLayoutHelp } from "./help";
|
|
22
18
|
import LayoutFooter from "./LayoutFooter";
|
|
@@ -24,15 +20,14 @@ import LayoutHeader from "./LayoutHeader";
|
|
|
24
20
|
import LayoutRail, { type LayoutAppLink } from "./LayoutRail";
|
|
25
21
|
import type { LayoutContext } from "./layout-context";
|
|
26
22
|
import type { LayoutBreadcrumb } from "./layout-runtime";
|
|
27
|
-
import PageTrailRow from "./PageTrailRow
|
|
28
|
-
import PageTrailRuntime from "./PageTrailRuntime.island";
|
|
23
|
+
import PageTrailRow from "./PageTrailRow";
|
|
29
24
|
import { derivePageTrail } from "./page-trail";
|
|
30
25
|
import { platformMessages } from "./platform-messages";
|
|
31
|
-
import RegisteredHelpDocuments from "./RegisteredHelpDocuments.island";
|
|
32
26
|
import { sortRailApps } from "./rail-navigation";
|
|
33
27
|
import { requestPath } from "./request-path";
|
|
34
28
|
import { getLocalizedRuntimeContext, type RuntimeContext } from "./runtime";
|
|
35
|
-
import
|
|
29
|
+
import ShellRuntime from "./ShellRuntime.island";
|
|
30
|
+
import { type ShellData, ShellDataProvider } from "./shell-data";
|
|
36
31
|
|
|
37
32
|
// Types
|
|
38
33
|
type Breadcrumb = LayoutBreadcrumb;
|
|
@@ -146,8 +141,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
146
141
|
const { c, title, fullPage, fullWidth, focusMode, flushCanvas, workspaceSidebarCollapsible } = props;
|
|
147
142
|
const runtime = getLocalizedRuntimeContext(c);
|
|
148
143
|
const cookie = c.req.raw.headers.get("Cookie") ?? "";
|
|
149
|
-
const theme =
|
|
150
|
-
c.get("page").theme = theme;
|
|
144
|
+
const theme = readThemePreferenceFromCookieHeader(cookie);
|
|
151
145
|
// One request-scoped locale drives <html lang>, the LocaleProvider below,
|
|
152
146
|
// and date formatting. The SSR seam and this component both resolve through
|
|
153
147
|
// the same canonical getLocale(c), so pages cannot diverge the three seams.
|
|
@@ -159,24 +153,16 @@ export default function Layout(props: LayoutProps) {
|
|
|
159
153
|
const currentRoute = resolveRuntimeRoute(runtime.apps, pathname);
|
|
160
154
|
const currentApp = currentRoute?.app;
|
|
161
155
|
const registeredHelp = getLayoutHelp(c);
|
|
156
|
+
const runtimeHelp = registeredHelp ? { documents: registeredHelp.documents, pageBase: registeredHelp.pageBase } : undefined;
|
|
162
157
|
const workspaceLayout = resolveAppWorkspaceLayoutForSidebar(
|
|
163
158
|
readAppWorkspaceLayoutCookie(cookie, currentApp?.id),
|
|
164
159
|
workspaceSidebarCollapsible,
|
|
165
160
|
);
|
|
166
161
|
const { primary: primaryApps, more: moreApps } = buildNavLinks(runtime.apps, user, lang);
|
|
167
162
|
const allApps = sortRailApps([...primaryApps, ...moreApps], lang);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
label: app.label,
|
|
172
|
-
href: app.href,
|
|
173
|
-
description: app.description,
|
|
174
|
-
accent: app.accent,
|
|
175
|
-
badge: app.badge,
|
|
176
|
-
}));
|
|
177
|
-
const searchLinks = navigationSearchItems(
|
|
178
|
-
runtime.apps,
|
|
179
|
-
allApps.map((app) => app.id),
|
|
163
|
+
const visibleAppIds = new Set(allApps.map((app) => app.id));
|
|
164
|
+
const searchLinks = Object.fromEntries(
|
|
165
|
+
runtime.apps.flatMap((app) => (visibleAppIds.has(app.id) && app.searchLinks?.length ? [[app.id, app.searchLinks] as const] : [])),
|
|
180
166
|
);
|
|
181
167
|
const searchHelpApps: GlobalSearchHelpApp[] = runtime.apps
|
|
182
168
|
.filter((app) => (app.searchTags?.length ?? 0) > 0)
|
|
@@ -227,6 +213,18 @@ export default function Layout(props: LayoutProps) {
|
|
|
227
213
|
[appAppearanceStyle(currentApp?.appearance), appWorkspaceLayoutStyle(workspaceLayout), focusMode && flushCanvas ? "padding:0" : ""]
|
|
228
214
|
.filter(Boolean)
|
|
229
215
|
.join(";") || undefined;
|
|
216
|
+
const appLabel = currentApp?.name ?? (pathname.startsWith("/admin") ? t.admin : undefined);
|
|
217
|
+
const shellData: ShellData = {
|
|
218
|
+
apps: allApps,
|
|
219
|
+
...(user
|
|
220
|
+
? { rail: c.get("railPreferences") ?? defaultRailPreferences(), profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref } }
|
|
221
|
+
: {}),
|
|
222
|
+
legalLinks,
|
|
223
|
+
theme,
|
|
224
|
+
accent: currentApp?.appearance?.accent,
|
|
225
|
+
searchLinks,
|
|
226
|
+
searchHelpApps,
|
|
227
|
+
};
|
|
230
228
|
if (focusMode) {
|
|
231
229
|
return (
|
|
232
230
|
<LocaleProvider locale={lang}>
|
|
@@ -239,10 +237,8 @@ export default function Layout(props: LayoutProps) {
|
|
|
239
237
|
data-layout-full-page="true"
|
|
240
238
|
data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
|
|
241
239
|
>
|
|
242
|
-
<
|
|
243
|
-
{registeredHelp && <RegisteredHelpDocuments documents={registeredHelp.documents} pageBase={registeredHelp.pageBase} />}
|
|
240
|
+
<ShellRuntime pageTrail help={runtimeHelp} />
|
|
244
241
|
<PageTrailProvider trail={pageTrail}>
|
|
245
|
-
<PageTrailRuntime />
|
|
246
242
|
<main class="min-h-0 min-w-0 flex-1">
|
|
247
243
|
<AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
|
|
248
244
|
</main>
|
|
@@ -266,65 +262,40 @@ export default function Layout(props: LayoutProps) {
|
|
|
266
262
|
data-layout-authenticated={showRail ? "true" : undefined}
|
|
267
263
|
data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
|
|
268
264
|
>
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
/>
|
|
291
|
-
)}
|
|
292
|
-
<div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col">
|
|
293
|
-
<LayoutHeader
|
|
294
|
-
appHref={appHref}
|
|
295
|
-
appLabel={currentApp?.name ?? (pathname.startsWith("/admin") ? t.admin : undefined)}
|
|
296
|
-
accent={currentApp?.appearance?.accent}
|
|
297
|
-
authenticated={showRail}
|
|
298
|
-
breadcrumbs={breadcrumbs}
|
|
299
|
-
homeLabel={t.home}
|
|
300
|
-
launchpadApps={launchpadApps}
|
|
301
|
-
legalLinks={legalLinks}
|
|
302
|
-
openAppsLabel={t.openApps}
|
|
303
|
-
profileName={profileName}
|
|
304
|
-
searchHelpApps={searchHelpApps}
|
|
305
|
-
searchLinks={searchLinks}
|
|
306
|
-
signInLabel={t.signIn}
|
|
307
|
-
theme={theme}
|
|
308
|
-
/>
|
|
309
|
-
{user && announcements && (
|
|
310
|
-
<GlobalAnnouncements
|
|
311
|
-
banners={announcements.banners}
|
|
312
|
-
announcements={announcements.announcements}
|
|
313
|
-
latestAnnouncementVersion={announcements.latestAnnouncementVersion}
|
|
314
|
-
cookieState={announcements.cookieState}
|
|
265
|
+
<ShellRuntime
|
|
266
|
+
workspace={{ appId: currentApp?.id }}
|
|
267
|
+
pageTrail
|
|
268
|
+
help={runtimeHelp}
|
|
269
|
+
commands={{ searchResources: !!user }}
|
|
270
|
+
push={!!user}
|
|
271
|
+
announcements={
|
|
272
|
+
user && announcements?.announcements.length
|
|
273
|
+
? { entries: announcements.announcements, latestVersion: announcements.latestAnnouncementVersion }
|
|
274
|
+
: undefined
|
|
275
|
+
}
|
|
276
|
+
banners={user && announcements?.banners.length ? true : undefined}
|
|
277
|
+
/>
|
|
278
|
+
<ShellDataProvider data={shellData}>
|
|
279
|
+
{showRail && <LayoutRail currentPath={requestPath(c)} />}
|
|
280
|
+
<div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col">
|
|
281
|
+
<LayoutHeader
|
|
282
|
+
authenticated={showRail}
|
|
283
|
+
title={appLabel ?? breadcrumbs.at(-1)?.title}
|
|
284
|
+
launchpad={!!(user || appLabel)}
|
|
285
|
+
breadcrumbs={user ? undefined : breadcrumbs}
|
|
315
286
|
/>
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
</
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
</
|
|
287
|
+
{user && announcements && <AnnouncementBanners banners={announcements.banners} />}
|
|
288
|
+
{user && <ProfileWarnings user={user} locale={lang} />}
|
|
289
|
+
{user && <ExpiryWarnings user={user} dateConfig={dateConfig} locale={lang} />}
|
|
290
|
+
<PageTrailProvider trail={pageTrail}>
|
|
291
|
+
<PageTrailRow trail={pageTrail} />
|
|
292
|
+
<main class={`layout-content-main min-h-0 min-w-0 flex-1 ${mainLayoutClass}`}>
|
|
293
|
+
<AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
|
|
294
|
+
</main>
|
|
295
|
+
</PageTrailProvider>
|
|
296
|
+
{!fullPage && !showRail && <LayoutFooter appName={settings?.app?.copyright || appName} legalLinks={legalLinks} />}
|
|
297
|
+
</div>
|
|
298
|
+
</ShellDataProvider>
|
|
328
299
|
</div>
|
|
329
300
|
</LocaleProvider>
|
|
330
301
|
);
|
package/src/ssr/LayoutHeader.tsx
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
import { ButtonLink, IconButtonLink } from "@k2b/ui";
|
|
2
|
-
import
|
|
3
|
-
import type { CloudTheme } from "../shared/theme";
|
|
4
|
-
import AppLaunchpad, { type AppLaunchpadApp } from "./AppLaunchpad.island";
|
|
5
|
-
import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
6
|
-
import GlobalSearchTrigger from "./GlobalSearchTrigger.island";
|
|
7
|
-
import LayoutBreadcrumbs from "./LayoutBreadcrumbs.island";
|
|
8
|
-
import LayoutHelpTrigger from "./LayoutHelpTrigger.island";
|
|
9
|
-
import LayoutPreferences from "./LayoutPreferences.island";
|
|
1
|
+
import { ButtonLink, IconButtonLink, useLocale } from "@k2b/ui";
|
|
2
|
+
import CloudShell from "./CloudShell.island";
|
|
10
3
|
import type { LayoutBreadcrumb } from "./layout-runtime";
|
|
4
|
+
import { platformMessages } from "./platform-messages";
|
|
11
5
|
|
|
12
6
|
type LayoutHeaderProps = {
|
|
13
|
-
accent?: string;
|
|
14
|
-
/** Set while the installation offers the mobile app. */
|
|
15
|
-
appHref?: string;
|
|
16
|
-
appLabel?: string;
|
|
17
7
|
authenticated: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
legalLinks: Array<{ label: string; href: string; icon?: string }>;
|
|
22
|
-
openAppsLabel: string;
|
|
23
|
-
profileName: string;
|
|
24
|
-
searchHelpApps: GlobalSearchHelpApp[];
|
|
25
|
-
searchLinks?: NavigationSearchItem[];
|
|
26
|
-
signInLabel: string;
|
|
27
|
-
theme: CloudTheme;
|
|
8
|
+
title?: string;
|
|
9
|
+
launchpad: boolean;
|
|
10
|
+
breadcrumbs?: LayoutBreadcrumb[];
|
|
28
11
|
};
|
|
29
12
|
|
|
30
13
|
/**
|
|
@@ -33,68 +16,39 @@ type LayoutHeaderProps = {
|
|
|
33
16
|
* and their profile through the rail, so the app area starts at the top edge.
|
|
34
17
|
* Anonymous pages have no rail: up to `lg` their header keeps the app name,
|
|
35
18
|
* help, search, and the app grid, and from `lg` it shows the breadcrumbs.
|
|
19
|
+
* Home and Sign in stay server markup, so focus on them survives the shell's
|
|
20
|
+
* mount; the shell renders the title and the controls between them.
|
|
36
21
|
*/
|
|
37
22
|
export default function LayoutHeader(props: LayoutHeaderProps) {
|
|
23
|
+
const locale = useLocale();
|
|
24
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
38
25
|
return (
|
|
39
26
|
<header
|
|
40
|
-
class={`layout-header paper flex min-h-[2.875rem] shrink-0 items-center
|
|
27
|
+
class={`layout-header paper flex min-h-[2.875rem] shrink-0 items-center px-2 py-1.5 md:px-3 md:py-2 ${props.authenticated ? "md:hidden" : ""}`}
|
|
41
28
|
style="box-shadow: var(--ui-shadow-surface)"
|
|
42
29
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
</IconButtonLink>
|
|
50
|
-
</div>
|
|
51
|
-
{!props.authenticated && (
|
|
52
|
-
<div class="hidden min-w-0 items-center lg:flex">
|
|
53
|
-
<LayoutBreadcrumbs breadcrumbs={props.breadcrumbs} />
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
56
|
-
<div class="flex min-w-0 items-center lg:hidden">
|
|
57
|
-
<span class="truncate font-semibold">{props.appLabel ?? props.breadcrumbs.at(-1)?.title}</span>
|
|
58
|
-
</div>
|
|
30
|
+
{/* A long title must not squeeze the button; `shrink-0` sits on a wrapper because the button's own
|
|
31
|
+
styles would win over a utility on the link. */}
|
|
32
|
+
<div class="flex shrink-0">
|
|
33
|
+
<IconButtonLink href="/" label={t().home}>
|
|
34
|
+
<img src="/branding/logo" alt="" class={props.authenticated ? "h-4 w-4" : "h-6 w-6"} />
|
|
35
|
+
</IconButtonLink>
|
|
59
36
|
</div>
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/>
|
|
74
|
-
{(props.authenticated || props.appLabel) && (
|
|
75
|
-
<AppLaunchpad
|
|
76
|
-
profile={
|
|
77
|
-
props.authenticated
|
|
78
|
-
? { name: props.profileName, theme: props.theme, ...(props.appHref ? { appHref: props.appHref } : {}) }
|
|
79
|
-
: undefined
|
|
80
|
-
}
|
|
81
|
-
apps={props.launchpadApps}
|
|
82
|
-
legalLinks={props.legalLinks}
|
|
83
|
-
variant="header"
|
|
84
|
-
label={props.openAppsLabel}
|
|
85
|
-
/>
|
|
86
|
-
)}
|
|
37
|
+
<CloudShell
|
|
38
|
+
slot="header"
|
|
39
|
+
authenticated={props.authenticated}
|
|
40
|
+
title={props.title}
|
|
41
|
+
launchpad={props.launchpad}
|
|
42
|
+
breadcrumbs={props.breadcrumbs}
|
|
43
|
+
/>
|
|
44
|
+
{!props.authenticated && (
|
|
45
|
+
<div class="ml-1 flex shrink-0">
|
|
46
|
+
<ButtonLink href="/auth/login" size="sm" variant="primary">
|
|
47
|
+
<i class="ti ti-login" aria-hidden="true" />
|
|
48
|
+
{t().signIn}
|
|
49
|
+
</ButtonLink>
|
|
87
50
|
</div>
|
|
88
|
-
|
|
89
|
-
<>
|
|
90
|
-
<LayoutPreferences initialTheme={props.theme} position="bottom-left" />
|
|
91
|
-
<ButtonLink href="/auth/login" size="sm" variant="primary">
|
|
92
|
-
<i class="ti ti-login" aria-hidden="true" />
|
|
93
|
-
{props.signInLabel}
|
|
94
|
-
</ButtonLink>
|
|
95
|
-
</>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
51
|
+
)}
|
|
98
52
|
</header>
|
|
99
53
|
);
|
|
100
54
|
}
|
package/src/ssr/LayoutHelp.tsx
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import type { HelpDocumentManifest, HelpDocumentPayload, HelpSearchPayload } from "@k2b/cloud/shared";
|
|
2
2
|
import { clipboard } from "@k2b/stdlib/solid";
|
|
3
3
|
import { Button, IconButton, IconButtonLink, MarkdownView, NoticeCard, Placeholder, prompts, ScrollArea, useLocale } from "@k2b/ui";
|
|
4
|
-
import { createEffect, createMemo, createSignal, For,
|
|
4
|
+
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
|
5
5
|
import { COMMANDS_CHANGED, contextCommandsWithShortcuts, shortcutLabel } from "../browser/command-shortcuts";
|
|
6
6
|
import { appAccentStyle } from "./app-appearance";
|
|
7
7
|
import { type GlobalSearchHelpApp, openGlobalSearchHelpDialog } from "./GlobalSearchHelpDialog";
|
|
8
8
|
import { helpMessages } from "./help-messages";
|
|
9
9
|
import { formatHelpBundleMarkdown, formatHelpDocumentMarkdown } from "./layout-help-markdown";
|
|
10
10
|
import { adjacentHelpDocuments, focusHelpArticleHeading, resetHelpArticleScroll } from "./layout-help-navigation";
|
|
11
|
+
import { HELP_TOPICS_EVENT, type HelpTopic, registry } from "./layout-help-registry";
|
|
11
12
|
import { searchHelpTopics } from "./layout-help-search";
|
|
12
13
|
import { layoutHelpTopicHref } from "./layout-help-url";
|
|
13
14
|
|
|
14
|
-
type HelpTopicBase = {
|
|
15
|
-
id: string;
|
|
16
|
-
title: string;
|
|
17
|
-
icon?: string;
|
|
18
|
-
description?: string;
|
|
19
|
-
order?: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type LayoutHelpDocumentsProps = {
|
|
23
|
-
documents: readonly HelpDocumentManifest[];
|
|
24
|
-
/** Canonical standalone Help route owned by the current app. */
|
|
25
|
-
pageBase: string;
|
|
26
|
-
};
|
|
27
15
|
export type LayoutHelpPageProps = {
|
|
28
16
|
documents: readonly HelpDocumentManifest[];
|
|
29
17
|
initialTopic?: string;
|
|
@@ -35,7 +23,6 @@ export type LayoutHelpPageProps = {
|
|
|
35
23
|
pageBase?: string;
|
|
36
24
|
};
|
|
37
25
|
|
|
38
|
-
type HelpTopic = (HelpTopicBase & { kind: "content"; children: JSX.Element }) | (HelpDocumentManifest & { kind: "document" });
|
|
39
26
|
type HelpView = "hub" | "search" | "article";
|
|
40
27
|
type HelpSession = {
|
|
41
28
|
view: HelpView;
|
|
@@ -51,23 +38,8 @@ type HelpSection = {
|
|
|
51
38
|
title: string;
|
|
52
39
|
};
|
|
53
40
|
|
|
54
|
-
const HELP_TOPICS_EVENT = "cloud:layout-help-topics";
|
|
55
41
|
const LAST_TOPIC_KEY = "cloud.layoutHelp.activeTopic";
|
|
56
42
|
|
|
57
|
-
declare global {
|
|
58
|
-
interface Window {
|
|
59
|
-
__cloudLayoutHelpTopics?: Map<string, HelpTopic>;
|
|
60
|
-
__cloudLayoutHelpPageBase?: string;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const registry = () => {
|
|
65
|
-
if (typeof window === "undefined") return null;
|
|
66
|
-
window.__cloudLayoutHelpTopics ??= new Map<string, HelpTopic>();
|
|
67
|
-
return window.__cloudLayoutHelpTopics;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const emitTopicsChanged = () => window.dispatchEvent(new Event(HELP_TOPICS_EVENT));
|
|
71
43
|
const iconClass = (icon?: string) => (icon?.startsWith("ti ") ? icon : `ti ${icon ?? "ti-circle"}`);
|
|
72
44
|
const legacyTopicContent = (topic: HelpTopic) => (topic.kind === "content" ? topic.children : null);
|
|
73
45
|
const sortedTopics = () =>
|
|
@@ -80,33 +52,6 @@ const mergeTopics = (registered: readonly HelpTopic[], documents: readonly HelpD
|
|
|
80
52
|
return [...topics.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title));
|
|
81
53
|
};
|
|
82
54
|
|
|
83
|
-
const registerTopic = (topic: HelpTopic) => {
|
|
84
|
-
const topics = registry();
|
|
85
|
-
if (!topics) return () => {};
|
|
86
|
-
topics.set(topic.id, topic);
|
|
87
|
-
emitTopicsChanged();
|
|
88
|
-
return () => {
|
|
89
|
-
if (topics.get(topic.id) !== topic) return;
|
|
90
|
-
topics.delete(topic.id);
|
|
91
|
-
emitTopicsChanged();
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** Register an app's server-prepared Markdown manifest with the shared help UI. */
|
|
96
|
-
export function LayoutHelpDocuments(props: LayoutHelpDocumentsProps) {
|
|
97
|
-
onMount(() => {
|
|
98
|
-
// Cleanup runs while this unmounts, when a parent `<Show>` may already have dropped the props' source.
|
|
99
|
-
const pageBase = props.pageBase;
|
|
100
|
-
window.__cloudLayoutHelpPageBase = pageBase;
|
|
101
|
-
const disposers = props.documents.map((document) => registerTopic({ ...document, kind: "document" }));
|
|
102
|
-
onCleanup(() => {
|
|
103
|
-
disposers.forEach((dispose) => dispose());
|
|
104
|
-
if (window.__cloudLayoutHelpPageBase === pageBase) delete window.__cloudLayoutHelpPageBase;
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
55
|
const Shortcuts = (props: { openSearchHelp: () => void }) => {
|
|
111
56
|
const locale = useLocale();
|
|
112
57
|
const t = () => helpMessages.resolve([locale()]).t;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IconButton, Tooltip, useLocale } from "@k2b/ui";
|
|
2
|
+
import { importOnDemand } from "../browser/reload";
|
|
3
|
+
import { createBusyAction } from "./busy-action";
|
|
4
|
+
import { platformMessages } from "./platform-messages";
|
|
5
|
+
import { useShellData } from "./shell-data";
|
|
6
|
+
|
|
7
|
+
type LayoutHelpTriggerProps = {
|
|
8
|
+
variant: "header" | "rail";
|
|
9
|
+
class?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Opens end-user help from either the desktop rail or the compact header. */
|
|
13
|
+
export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
|
|
14
|
+
const locale = useLocale();
|
|
15
|
+
const t = () => platformMessages.resolve([locale()]).t;
|
|
16
|
+
const data = useShellData();
|
|
17
|
+
const action = createBusyAction();
|
|
18
|
+
const openHelp = () =>
|
|
19
|
+
action.run(
|
|
20
|
+
() => importOnDemand(() => import("./LayoutHelp")),
|
|
21
|
+
(help) => help.openLayoutHelpDialog(data?.searchHelpApps ?? [], data?.accent),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (props.variant === "rail")
|
|
25
|
+
return (
|
|
26
|
+
<Tooltip.Trigger
|
|
27
|
+
type="button"
|
|
28
|
+
class={`rail-item text-blue-500 hover:bg-blue-500/10 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/15 dark:hover:text-blue-300 ${props.class ?? ""}`}
|
|
29
|
+
onClick={openHelp}
|
|
30
|
+
aria-busy={action.busy() ? "true" : undefined}
|
|
31
|
+
aria-label={t().openHelp}
|
|
32
|
+
placement="right"
|
|
33
|
+
delay={0}
|
|
34
|
+
content={t().helpShortcut}
|
|
35
|
+
>
|
|
36
|
+
<i class={`${action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-help-circle"} text-base`} />
|
|
37
|
+
</Tooltip.Trigger>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<IconButton
|
|
42
|
+
class={props.class}
|
|
43
|
+
onClick={openHelp}
|
|
44
|
+
aria-busy={action.busy() ? "true" : undefined}
|
|
45
|
+
label={t().openHelp}
|
|
46
|
+
title={t().helpShortcut}
|
|
47
|
+
>
|
|
48
|
+
<i class={`${action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-help-circle"} text-base`} />
|
|
49
|
+
</IconButton>
|
|
50
|
+
);
|
|
51
|
+
}
|