@k2b/cloud 0.31.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.
Files changed (173) hide show
  1. package/package.json +8 -6
  2. package/src/_internal/define-app.ts +20 -9
  3. package/src/_internal/page-responses.ts +5 -1
  4. package/src/ai/app-skills.ts +1 -7
  5. package/src/ai/audio-tool.ts +2 -1
  6. package/src/ai/browser.ts +109 -0
  7. package/src/ai/capabilities.ts +6 -2
  8. package/src/ai/chart-block.ts +431 -81
  9. package/src/ai/chat/action-sentences.ts +13 -7
  10. package/src/ai/chat/blocks.tsx +11 -7
  11. package/src/ai/chat/builtin-tools.tsx +9 -7
  12. package/src/ai/chat/chart-block.tsx +218 -44
  13. package/src/ai/chat/explorer-block.tsx +276 -0
  14. package/src/ai/chat/file-tools.tsx +40 -1
  15. package/src/ai/chat/message-actions.tsx +6 -0
  16. package/src/ai/chat/message-utils.ts +7 -0
  17. package/src/ai/chat/messages.ts +16 -0
  18. package/src/ai/chat/presentation.tsx +13 -3
  19. package/src/ai/chat/reference-text.tsx +34 -0
  20. package/src/ai/chat/tool-groups.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +14 -0
  22. package/src/ai/chat/turn-view.tsx +17 -7
  23. package/src/ai/chat/view-block.browser-harness.tsx +35 -0
  24. package/src/ai/chat/view-block.tsx +417 -0
  25. package/src/ai/chat/visual-tools.tsx +32 -53
  26. package/src/ai/client-tool-results.ts +9 -0
  27. package/src/ai/code-source-contracts.ts +17 -2
  28. package/src/ai/code-source-tools.ts +1 -0
  29. package/src/ai/default-tool-contracts.ts +334 -91
  30. package/src/ai/default-tools.ts +89 -13
  31. package/src/ai/executor.ts +173 -31
  32. package/src/ai/file-error.ts +2 -0
  33. package/src/ai/file-tools.ts +27 -7
  34. package/src/ai/files-store.ts +60 -0
  35. package/src/ai/html-pdf-tool.ts +3 -2
  36. package/src/ai/http.ts +37 -12
  37. package/src/ai/index.ts +90 -0
  38. package/src/ai/local-tool-contracts.ts +244 -0
  39. package/src/ai/malformed-tool-call-repair.ts +144 -0
  40. package/src/ai/markdown-pdf-tool.ts +2 -1
  41. package/src/ai/memories.ts +48 -1
  42. package/src/ai/memory-learning.ts +17 -6
  43. package/src/ai/memory-tool.ts +1 -0
  44. package/src/ai/migrate.ts +15 -0
  45. package/src/ai/openai-api-messages.ts +53 -0
  46. package/src/ai/openai-api-slots.ts +40 -0
  47. package/src/ai/openai-api.ts +383 -0
  48. package/src/ai/protocol.ts +6 -2
  49. package/src/ai/provider.ts +17 -4
  50. package/src/ai/request-context.ts +33 -0
  51. package/src/ai/routes.ts +96 -57
  52. package/src/ai/runtime.ts +48 -12
  53. package/src/ai/settings.ts +5 -0
  54. package/src/ai/skill-catalog.ts +20 -5
  55. package/src/ai/skill-format.ts +17 -7
  56. package/src/ai/skill-tool.ts +15 -5
  57. package/src/ai/skills-routes.ts +20 -6
  58. package/src/ai/skills.ts +26 -7
  59. package/src/ai/store.ts +25 -0
  60. package/src/ai/system-prompt.ts +69 -10
  61. package/src/ai/timeline.ts +3 -1
  62. package/src/ai/tools.ts +3 -0
  63. package/src/ai/types.ts +9 -0
  64. package/src/ai/view-block.ts +941 -0
  65. package/src/ai/vision-tool.ts +2 -1
  66. package/src/api/admin-identity.ts +6 -1
  67. package/src/api/app-approval-actions.ts +36 -0
  68. package/src/api/app-approval.ts +10 -2
  69. package/src/api/approvals-loader.ts +15 -0
  70. package/src/api/approvals.ts +120 -0
  71. package/src/api/index.ts +3 -0
  72. package/src/api/me.ts +12 -3
  73. package/src/approvals/Approvals.browser-harness.tsx +16 -0
  74. package/src/approvals/Approvals.tsx +621 -0
  75. package/src/approvals/ApprovalsNavStatus.tsx +20 -0
  76. package/src/approvals/messages.ts +95 -0
  77. package/src/approvals/ui.ts +8 -0
  78. package/src/browser/app-approval.ts +32 -0
  79. package/src/browser/choose-files.tsx +26 -13
  80. package/src/browser/commands.ts +13 -4
  81. package/src/browser/file-provider-error.ts +11 -0
  82. package/src/browser/file-providers.ts +3 -12
  83. package/src/browser/files.tsx +1 -2
  84. package/src/browser/live-connection.ts +2 -3
  85. package/src/browser/navigation-search.ts +4 -1
  86. package/src/browser/provider-list.ts +2 -1
  87. package/src/browser/save-files.tsx +13 -2
  88. package/src/cli/account.ts +21 -2
  89. package/src/cli/commands.ts +4 -2
  90. package/src/contracts/app-approval.ts +120 -0
  91. package/src/contracts/approval-paths.ts +4 -0
  92. package/src/contracts/approvals.ts +81 -0
  93. package/src/contracts/command-path.ts +17 -0
  94. package/src/contracts/commands.ts +5 -15
  95. package/src/contracts/shared.ts +5 -0
  96. package/src/events/live-protocol.ts +2 -1
  97. package/src/events/live-server-message.ts +40 -0
  98. package/src/events/testing.ts +88 -0
  99. package/src/server/index.ts +2 -0
  100. package/src/server/middleware/auth.ts +11 -1
  101. package/src/server/request-timeout.ts +22 -0
  102. package/src/server/services/access.ts +4 -3
  103. package/src/services/app-approval.ts +103 -15
  104. package/src/services/approvals/cursor.ts +15 -0
  105. package/src/services/approvals/device.ts +112 -0
  106. package/src/services/approvals/index.ts +27 -11
  107. package/src/services/approvals/list.ts +120 -0
  108. package/src/services/approvals/migrate.ts +9 -1
  109. package/src/services/identity/models-only-auth.ts +25 -0
  110. package/src/services/notifications/email-frame.ts +16 -3
  111. package/src/services/outbox.ts +13 -6
  112. package/src/services/service-account-credentials.ts +22 -10
  113. package/src/services/settings/core-settings.ts +27 -0
  114. package/src/shared/ai-platform-prompt.ts +29 -3
  115. package/src/shared/email-html.ts +4 -0
  116. package/src/shared/index.ts +6 -1
  117. package/src/shared/markdown/extensions/code.ts +14 -0
  118. package/src/shared/markdown/index.ts +4 -4
  119. package/src/shared/theme.ts +41 -20
  120. package/src/ssr/AnnouncementBanners.tsx +30 -0
  121. package/src/ssr/AppBadge.tsx +4 -4
  122. package/src/ssr/AppLaunchpadPanel.tsx +6 -6
  123. package/src/ssr/CloudShell.island.tsx +91 -0
  124. package/src/ssr/GlobalSearchTrigger.tsx +51 -0
  125. package/src/ssr/Layout.tsx +56 -85
  126. package/src/ssr/LayoutHeader.tsx +31 -77
  127. package/src/ssr/LayoutHelp.tsx +2 -57
  128. package/src/ssr/LayoutHelpTrigger.tsx +51 -0
  129. package/src/ssr/{LayoutPreferences.island.tsx → LayoutPreferences.tsx} +2 -2
  130. package/src/ssr/LayoutRail.tsx +15 -69
  131. package/src/ssr/MinimalLayout.tsx +2 -3
  132. package/src/ssr/MinimalLayoutPreferences.island.tsx +12 -15
  133. package/src/ssr/MobileNavigation.tsx +13 -8
  134. package/src/ssr/MobileProfileActions.tsx +10 -7
  135. package/src/ssr/PageTrailRow.tsx +14 -0
  136. package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +12 -7
  137. package/src/ssr/PwaLayout.tsx +0 -2
  138. package/src/ssr/PwaRuntime.island.tsx +1 -1
  139. package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +2 -2
  140. package/src/ssr/ShellRuntime.island.tsx +170 -0
  141. package/src/ssr/TimezoneCookie.island.tsx +3 -19
  142. package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
  143. package/src/ssr/announcements-dialog.tsx +66 -0
  144. package/src/ssr/app-badges.ts +50 -6
  145. package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
  146. package/src/ssr/busy-action.ts +19 -0
  147. package/src/ssr/islands/index.ts +9 -6
  148. package/src/ssr/launchpad-surface.tsx +15 -0
  149. package/src/ssr/layout-help-registry.tsx +60 -0
  150. package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
  151. package/src/ssr/page-trail.browser-harness.tsx +52 -3
  152. package/src/ssr/preference-controller.ts +30 -21
  153. package/src/ssr/profile-actions.ts +26 -15
  154. package/src/ssr/profile-preferences-messages.ts +8 -4
  155. package/src/ssr/rail-context.ts +8 -12
  156. package/src/ssr/rail-navigation.ts +5 -3
  157. package/src/ssr/runtime.ts +6 -1
  158. package/src/ssr/shell-data.tsx +48 -0
  159. package/src/ssr/timezone-cookie.ts +22 -0
  160. package/src/ssr/workspace-navigation.ts +25 -2
  161. package/src/styles/approvals.css +390 -0
  162. package/src/styles/global.css +22 -7
  163. package/src/styles/utilities-navigation.css +13 -0
  164. package/src/workflows/language/compiler.ts +1 -1
  165. package/src/ssr/AppWorkspaceController.island.tsx +0 -21
  166. package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
  167. package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
  169. package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
  170. package/src/ssr/PageTrailRow.island.tsx +0 -20
  171. package/src/ssr/PageTrailRuntime.island.tsx +0 -26
  172. package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
  173. /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
@@ -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 { readThemeFromCookieHeader } from "../shared/theme";
14
- import type { AppLaunchpadApp } from "./AppLaunchpad.island";
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.island";
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 TimezoneCookie from "./TimezoneCookie.island";
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 = readThemeFromCookieHeader(cookie);
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 launchpadApps: AppLaunchpadApp[] = allApps.map((app) => ({
169
- id: app.id,
170
- iconClass: app.iconClass,
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
- <TimezoneCookie />
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
- <TimezoneCookie />
270
- {registeredHelp && <RegisteredHelpDocuments documents={registeredHelp.documents} pageBase={registeredHelp.pageBase} />}
271
- <AppWorkspaceController appId={currentApp?.id} />
272
- {user && <BrowserPushRegistration />}
273
- {showRail && (
274
- <LayoutRail
275
- accent={currentApp?.appearance?.accent}
276
- appHref={appHref}
277
- appsLabel={t.apps}
278
- homeLabel={t.home}
279
- launchpadApps={launchpadApps}
280
- legalLinks={legalLinks}
281
- openAppsLabel={t.openApps}
282
- apps={allApps}
283
- railSettings={c.get("railPreferences") ?? defaultRailPreferences()}
284
- currentPath={requestPath(c)}
285
- profileAvatarSrc={profileAvatarSrc}
286
- profileName={profileName}
287
- searchHelpApps={searchHelpApps}
288
- searchLinks={searchLinks}
289
- theme={theme}
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
- {user && <ProfileWarnings user={user} locale={lang} />}
318
- {user && <ExpiryWarnings user={user} dateConfig={dateConfig} locale={lang} />}
319
- <PageTrailProvider trail={pageTrail}>
320
- <PageTrailRuntime />
321
- <PageTrailRow />
322
- <main class={`layout-content-main min-h-0 min-w-0 flex-1 ${mainLayoutClass}`}>
323
- <AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
324
- </main>
325
- </PageTrailProvider>
326
- {!fullPage && !showRail && <LayoutFooter appName={settings?.app?.copyright || appName} legalLinks={legalLinks} />}
327
- </div>
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
  );
@@ -1,30 +1,13 @@
1
- import { ButtonLink, IconButtonLink } from "@k2b/ui";
2
- import type { NavigationSearchItem } from "../browser/navigation-search";
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
- breadcrumbs: LayoutBreadcrumb[];
19
- homeLabel: string;
20
- launchpadApps: AppLaunchpadApp[];
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 justify-between px-2 py-1.5 md:px-3 md:py-2 ${props.authenticated ? "md:hidden" : ""}`}
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
- <div class="flex min-w-0 items-center gap-2">
44
- {/* A long title must not squeeze the button; `shrink-0` sits on a wrapper because the button's own
45
- styles would win over a utility on the link. */}
46
- <div class="flex shrink-0">
47
- <IconButtonLink href="/" label={props.homeLabel}>
48
- <img src="/branding/logo" alt="" class={props.authenticated ? "h-4 w-4" : "h-6 w-6"} />
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
- <div class="flex shrink-0 items-center gap-1">
61
- <div class="flex items-center gap-1 lg:hidden">
62
- <LayoutHelpTrigger
63
- variant="header"
64
- registerCommand={!props.authenticated}
65
- searchHelpApps={props.searchHelpApps}
66
- accent={props.accent}
67
- />
68
- <GlobalSearchTrigger
69
- variant={props.authenticated ? "header" : "host"}
70
- registerCommand
71
- searchLinks={props.searchLinks}
72
- searchResources={props.authenticated}
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
- {!props.authenticated && (
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
  }
@@ -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, type JSX, onCleanup, onMount, Show } from "solid-js";
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
+ }
@@ -1,9 +1,9 @@
1
1
  import { Dropdown, type DropdownPosition, useLocale } from "@k2b/ui";
2
- import type { CloudTheme } from "../shared/theme";
2
+ import type { CloudThemePreference } from "../shared/theme";
3
3
  import { createPreferenceController } from "./preference-controller";
4
4
 
5
5
  type LayoutPreferencesProps = {
6
- initialTheme: CloudTheme;
6
+ initialTheme: CloudThemePreference;
7
7
  position: DropdownPosition;
8
8
  };
9
9
 
@@ -1,75 +1,21 @@
1
- import type { NavigationSearchItem } from "../browser/navigation-search";
2
- import type { RailSnapshot } from "../contracts/rail-preferences";
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 LayoutHelpTrigger from "./LayoutHelpTrigger.island";
8
- import ProfilePreferences from "./ProfilePreferences.island";
9
- import RailApps from "./RailApps.island";
1
+ import { useLocale } from "@k2b/ui";
2
+ import CloudShell from "./CloudShell.island";
3
+ import { platformMessages } from "./platform-messages";
10
4
  import type { RailApp } from "./rail-navigation";
11
5
  export type LayoutAppLink = RailApp;
12
6
 
13
- type LayoutRailProps = {
14
- accent?: string;
15
- /** Set while the installation offers the mobile app. */
16
- appHref?: string;
17
- appsLabel: string;
18
- homeLabel: string;
19
- launchpadApps: AppLaunchpadApp[];
20
- legalLinks: Array<{ label: string; href: string; icon?: string }>;
21
- openAppsLabel: string;
22
- apps: RailApp[];
23
- railSettings: RailSnapshot;
24
- /** `pathname + search` of the request; the island resolves it against the browser origin. */
25
- currentPath: string;
26
- profileAvatarSrc?: string;
27
- profileName: string;
28
- searchHelpApps: GlobalSearchHelpApp[];
29
- searchLinks?: NavigationSearchItem[];
30
- theme: CloudTheme;
31
- };
32
-
33
- const jsonScript = (value: unknown): string => JSON.stringify(value).replace(/</g, "\\u003c");
34
-
35
- export default function LayoutRail(props: LayoutRailProps) {
36
- const profile = { name: props.profileName, theme: props.theme, ...(props.appHref ? { appHref: props.appHref } : {}) };
7
+ /** The rail frame and Home stay server markup; only the navigation is interactive. */
8
+ export default function LayoutRail(props: { currentPath: string }) {
9
+ const locale = useLocale();
10
+ const t = () => platformMessages.resolve([locale()]).t;
37
11
  return (
38
- <>
39
- <AppLaunchpad profile={profile} apps={props.launchpadApps} legalLinks={props.legalLinks} />
40
- <script id="cloud-app-launchpad-data" type="application/json">
41
- {jsonScript({ apps: props.launchpadApps, legalLinks: props.legalLinks, profile })}
42
- </script>
43
- <script id="cloud-rail-data" type="application/json">
44
- {jsonScript({ apps: props.apps, settings: props.railSettings })}
45
- </script>
46
- <aside class="layout-rail hidden w-10 shrink-0 flex-col md:flex">
47
- <div class="layout-rail-logo flex h-[2.875rem] shrink-0 items-center justify-center">
48
- <a href="/" aria-label={props.homeLabel}>
49
- <img src="/branding/logo" alt="Logo" class="h-5 w-5" />
50
- </a>
51
- </div>
52
- <nav class="layout-rail-navigation flex min-h-0 flex-1 flex-col items-center gap-1" aria-label={props.appsLabel}>
53
- <RailApps apps={props.apps} settings={props.railSettings} currentPath={props.currentPath} />
54
- <AppLaunchpad
55
- profile={profile}
56
- apps={props.launchpadApps}
57
- legalLinks={props.legalLinks}
58
- variant="rail"
59
- label={props.openAppsLabel}
60
- />
61
- <div class="mt-auto shrink-0 flex flex-col items-center gap-1">
62
- <GlobalSearchTrigger variant="rail" searchLinks={props.searchLinks} />
63
- <LayoutHelpTrigger variant="rail" registerCommand searchHelpApps={props.searchHelpApps} accent={props.accent} />
64
- <ProfilePreferences
65
- appHref={props.appHref}
66
- avatarSrc={props.profileAvatarSrc}
67
- initialTheme={props.theme}
68
- name={props.profileName}
69
- />
70
- </div>
71
- </nav>
72
- </aside>
73
- </>
12
+ <aside class="layout-rail hidden w-10 shrink-0 flex-col md:flex">
13
+ <div class="layout-rail-logo flex h-[2.875rem] shrink-0 items-center justify-center">
14
+ <a href="/" aria-label={t().home}>
15
+ <img src="/branding/logo" alt="Logo" class="h-5 w-5" />
16
+ </a>
17
+ </div>
18
+ <CloudShell slot="rail" currentPath={props.currentPath} />
19
+ </aside>
74
20
  );
75
21
  }
@@ -2,7 +2,7 @@ import { LocaleProvider } from "@k2b/ui";
2
2
  import type { JSX } from "solid-js/jsx-runtime";
3
3
  import { getLocale } from "../server/locale";
4
4
  import { resolveAppPresentations } from "../shared/app-presentation";
5
- import { readThemeFromCookieHeader } from "../shared/theme";
5
+ import { readThemePreferenceFromCookieHeader } from "../shared/theme";
6
6
  import type { MinimalLayoutContext } from "./layout-context";
7
7
  import MinimalLayoutPreferences from "./MinimalLayoutPreferences.island";
8
8
  import { profilePreferencesMessages } from "./profile-preferences-messages";
@@ -26,9 +26,8 @@ export type MinimalLayoutProps = {
26
26
 
27
27
  export default function MinimalLayout(props: MinimalLayoutProps) {
28
28
  const cookie = props.c.req.raw.headers.get("Cookie") ?? "";
29
- const theme = readThemeFromCookieHeader(cookie);
29
+ const theme = readThemePreferenceFromCookieHeader(cookie);
30
30
  const locale = getLocale(props.c);
31
- props.c.get("page").theme = theme;
32
31
 
33
32
  if (props.preferences === false) {
34
33
  return (