@k2b/cloud 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +8 -0
  2. package/package.json +12 -5
  3. package/scripts/build-canvas-workers.ts +109 -29
  4. package/scripts/build.ts +3 -5
  5. package/src/_internal/app-favicon.ts +4 -2
  6. package/src/_internal/app-path.ts +25 -0
  7. package/src/_internal/app-presentation.ts +4 -0
  8. package/src/_internal/canvas-worker.ts +5 -3
  9. package/src/_internal/capabilities.ts +72 -48
  10. package/src/_internal/capability-reviews.ts +81 -0
  11. package/src/_internal/capability-schemas.ts +29 -0
  12. package/src/_internal/capability-sentences.ts +216 -1
  13. package/src/_internal/capability-streams.ts +7 -0
  14. package/src/_internal/define-app.ts +63 -16
  15. package/src/_internal/font-preloads.ts +2 -1
  16. package/src/_internal/panel-cookie.ts +39 -0
  17. package/src/_internal/registry-validation.ts +21 -26
  18. package/src/_internal/runtime-context.ts +1 -0
  19. package/src/_internal/runtime-watcher.ts +6 -0
  20. package/src/_internal/static-assets.ts +52 -3
  21. package/src/_internal/status-preserving-ssr.ts +2 -0
  22. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  23. package/src/access/PermissionEditor.tsx +32 -7
  24. package/src/access/ResourceApiKeys.tsx +5 -1
  25. package/src/account/EntitySearch.tsx +1 -1
  26. package/src/ai/action-review.ts +101 -0
  27. package/src/ai/approval-routes.ts +5 -2
  28. package/src/ai/approvals.ts +139 -28
  29. package/src/ai/background-review.ts +72 -0
  30. package/src/ai/browser.ts +10 -0
  31. package/src/ai/capabilities.ts +29 -11
  32. package/src/ai/capability-execution.ts +76 -43
  33. package/src/ai/capability-result.ts +95 -0
  34. package/src/ai/chat/action-card.ts +398 -0
  35. package/src/ai/chat/action-messages.ts +129 -0
  36. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  37. package/src/ai/chat/action-question.fixtures.ts +733 -0
  38. package/src/ai/chat/action-question.tsx +669 -0
  39. package/src/ai/chat/action-sentences.ts +1 -44
  40. package/src/ai/chat/blocks.tsx +19 -25
  41. package/src/ai/chat/message-actions.tsx +6 -0
  42. package/src/ai/chat/tool-action.tsx +481 -0
  43. package/src/ai/chat/turn-view.tsx +45 -163
  44. package/src/ai/chat/view-block.tsx +22 -7
  45. package/src/ai/chat-task-contracts.ts +31 -3
  46. package/src/ai/chat-task-reasons-migrate.ts +75 -0
  47. package/src/ai/chat-task-reasons.ts +92 -0
  48. package/src/ai/chat-tasks.ts +438 -78
  49. package/src/ai/client/controller.ts +59 -0
  50. package/src/ai/default-tools.ts +9 -1
  51. package/src/ai/executor.ts +190 -39
  52. package/src/ai/index.ts +14 -0
  53. package/src/ai/migrate.ts +39 -4
  54. package/src/ai/projects-routes.ts +3 -0
  55. package/src/ai/protocol.ts +14 -2
  56. package/src/ai/recent-resources.ts +240 -0
  57. package/src/ai/recent-sent-refs.ts +38 -0
  58. package/src/ai/request-context.ts +7 -1
  59. package/src/ai/routes.ts +172 -2
  60. package/src/ai/runtime.ts +13 -27
  61. package/src/ai/skills-routes.ts +3 -0
  62. package/src/ai/skills.ts +10 -1
  63. package/src/ai/store.ts +232 -27
  64. package/src/ai/timeline.ts +9 -1
  65. package/src/ai/tool-audit.ts +8 -0
  66. package/src/ai/tool-wording.ts +67 -0
  67. package/src/ai/turn-abort.ts +33 -0
  68. package/src/ai/types.ts +40 -4
  69. package/src/api/accounts-entities.ts +2 -1
  70. package/src/api/admin-ai-projects.ts +2 -0
  71. package/src/api/admin-ai-skills.ts +2 -0
  72. package/src/api/admin-ai-usage.ts +2 -0
  73. package/src/api/admin-core-settings.ts +190 -139
  74. package/src/api/admin-geoip.ts +80 -0
  75. package/src/api/admin-identity.ts +3 -0
  76. package/src/api/admin-lifecycle.ts +3 -0
  77. package/src/api/admin-outgoing-mail.ts +6 -2
  78. package/src/api/app-approval.ts +54 -6
  79. package/src/api/approvals-loader.ts +23 -1
  80. package/src/api/approvals.ts +3 -1
  81. package/src/api/auth.ts +25 -6
  82. package/src/api/capabilities.ts +44 -11
  83. package/src/api/capability-streams.ts +12 -0
  84. package/src/api/help.ts +4 -28
  85. package/src/api/index.ts +7 -1
  86. package/src/api/lasting-access-messages.ts +17 -0
  87. package/src/api/mcp.ts +3 -0
  88. package/src/api/me-panel-consent.ts +71 -0
  89. package/src/api/me.ts +80 -33
  90. package/src/api/search.ts +3 -1
  91. package/src/api/sync-ops.ts +2 -0
  92. package/src/api/widgets.ts +3 -0
  93. package/src/approvals/Approvals.tsx +180 -238
  94. package/src/approvals/messages.ts +12 -24
  95. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  96. package/src/browser/CloudResourceSearch.tsx +10 -6
  97. package/src/browser/app-approval.ts +60 -6
  98. package/src/browser/commands.ts +22 -6
  99. package/src/browser/composer-catalog.ts +145 -0
  100. package/src/browser/composer-insert-messages.ts +31 -0
  101. package/src/browser/composer-insert.ts +160 -0
  102. package/src/browser/composer-open-messages.ts +17 -0
  103. package/src/browser/composer-open.ts +110 -0
  104. package/src/browser/discoverable-command.ts +6 -0
  105. package/src/browser/hints.ts +6 -0
  106. package/src/browser/live-connection.ts +26 -2
  107. package/src/browser/navigation-apps.ts +7 -0
  108. package/src/browser/panel-context-bridge.ts +30 -0
  109. package/src/browser/panel-context-receiver.ts +53 -0
  110. package/src/browser/panel-menu.tsx +56 -0
  111. package/src/browser/panel-messages.ts +35 -0
  112. package/src/browser/panel-shortcuts.ts +31 -0
  113. package/src/browser/panel-slot.ts +756 -0
  114. package/src/browser/panel.ts +65 -0
  115. package/src/browser/resource-picker.tsx +12 -1
  116. package/src/browser/search-commands.ts +6 -3
  117. package/src/capabilities/client.ts +8 -2
  118. package/src/capabilities/reviews.ts +205 -0
  119. package/src/capabilities/server.ts +10 -3
  120. package/src/capabilities/testing.ts +47 -0
  121. package/src/capabilities/types.ts +2 -2
  122. package/src/cli/account.ts +13 -3
  123. package/src/cli/admin/geoip.ts +115 -0
  124. package/src/cli/admin/index.ts +3 -0
  125. package/src/contracts/app-approval.ts +159 -12
  126. package/src/contracts/app.ts +14 -0
  127. package/src/contracts/approvals.ts +2 -2
  128. package/src/contracts/capabilities.ts +410 -18
  129. package/src/contracts/geoip.ts +42 -0
  130. package/src/contracts/index.ts +3 -0
  131. package/src/contracts/panel-context-plain.ts +99 -0
  132. package/src/contracts/panel-context.ts +93 -0
  133. package/src/contracts/registry.ts +3 -1
  134. package/src/contracts/seen-hints.ts +35 -0
  135. package/src/contracts/shared.ts +2 -0
  136. package/src/server/client-ip.ts +5 -0
  137. package/src/server/help-payload.ts +38 -0
  138. package/src/server/middleware/auth.ts +11 -1
  139. package/src/server/middleware/middleware.ts +9 -0
  140. package/src/server/middleware/no-transform.ts +19 -0
  141. package/src/server/middleware/rate-limit.ts +4 -6
  142. package/src/server/request-context/address.ts +45 -0
  143. package/src/server/request-context/geoip.ts +380 -0
  144. package/src/server/request-context/index.ts +33 -0
  145. package/src/server/request-context/user-agent.ts +95 -0
  146. package/src/server/services/access.ts +5 -1
  147. package/src/services/accounts/posix.ts +2 -1
  148. package/src/services/app-approval.ts +533 -90
  149. package/src/services/approvals/device.ts +9 -5
  150. package/src/services/approvals/index.ts +6 -5
  151. package/src/services/approvals/list.ts +26 -5
  152. package/src/services/auth-flows/notification-sender.ts +3 -0
  153. package/src/services/geoip/index.ts +164 -0
  154. package/src/services/geoip/store.ts +30 -0
  155. package/src/services/geoip/upload.ts +150 -0
  156. package/src/services/identity/routes.ts +5 -1
  157. package/src/services/index.ts +32 -3
  158. package/src/services/known-browsers.ts +312 -0
  159. package/src/services/login-address-store.ts +51 -0
  160. package/src/services/login-context.ts +118 -0
  161. package/src/services/mandates/index.ts +3 -0
  162. package/src/services/mandates/policy.ts +15 -3
  163. package/src/services/notifications/browser.ts +16 -5
  164. package/src/services/notifications/email-frame.ts +1 -1
  165. package/src/services/oauth-tokens.ts +16 -2
  166. package/src/services/panel-context-consent.ts +58 -0
  167. package/src/services/pdf/image-protocol.ts +135 -0
  168. package/src/services/pdf/image-variants.ts +124 -0
  169. package/src/services/pdf/image-worker.ts +31 -0
  170. package/src/services/pdf/image.ts +109 -0
  171. package/src/services/pdf/index.ts +9 -0
  172. package/src/services/service-account-credentials.ts +21 -0
  173. package/src/services/session/index.ts +97 -16
  174. package/src/services/session/recent.ts +13 -0
  175. package/src/services/settings/api.ts +11 -6
  176. package/src/services/settings/app.ts +5 -4
  177. package/src/services/settings/core-settings.ts +4 -3
  178. package/src/services/settings/index.ts +19 -17
  179. package/src/services/settings/namespace.ts +2 -1
  180. package/src/services/settings/store.ts +83 -11
  181. package/src/services/webauthn.ts +12 -0
  182. package/src/shared/ai-platform-prompt.ts +1 -1
  183. package/src/shared/app-presentation.ts +1 -0
  184. package/src/shared/email-html.ts +2 -0
  185. package/src/shared/help.ts +3 -0
  186. package/src/shared/redirect.ts +12 -0
  187. package/src/shared-text/cursor.ts +22 -0
  188. package/src/shared-text/documents.ts +142 -0
  189. package/src/shared-text/events.ts +43 -0
  190. package/src/shared-text/index.ts +12 -0
  191. package/src/shared-text/log.ts +48 -0
  192. package/src/shared-text/text-diff.ts +23 -0
  193. package/src/shared-text/types.ts +41 -0
  194. package/src/ssr/AdminLayout.tsx +2 -0
  195. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  196. package/src/ssr/CloudShell.island.tsx +95 -10
  197. package/src/ssr/Layout.tsx +66 -10
  198. package/src/ssr/LayoutHeader.tsx +15 -9
  199. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  200. package/src/ssr/LayoutHelp.tsx +435 -274
  201. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
  202. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  203. package/src/ssr/LayoutRail.tsx +2 -2
  204. package/src/ssr/LayoutTabBar.tsx +29 -0
  205. package/src/ssr/MobileNavigation.tsx +2 -2
  206. package/src/ssr/PanelLayout.tsx +35 -0
  207. package/src/ssr/PanelRuntime.island.tsx +94 -0
  208. package/src/ssr/PanelSlot.tsx +158 -0
  209. package/src/ssr/PanelTrigger.tsx +49 -0
  210. package/src/ssr/ShellRuntime.island.tsx +69 -5
  211. package/src/ssr/app-appearance.ts +3 -2
  212. package/src/ssr/app-color.ts +105 -0
  213. package/src/ssr/app-launchpad.tsx +18 -1
  214. package/src/ssr/app-navigation.ts +28 -0
  215. package/src/ssr/compact-shell.baseline.json +5270 -0
  216. package/src/ssr/compact-shell.browser-harness.ts +439 -0
  217. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  218. package/src/ssr/help-messages.ts +4 -0
  219. package/src/ssr/help.ts +20 -0
  220. package/src/ssr/hints.ts +25 -0
  221. package/src/ssr/index.ts +5 -0
  222. package/src/ssr/islands/index.ts +4 -3
  223. package/src/ssr/layout-context.ts +2 -0
  224. package/src/ssr/layout-help-article.server.ts +50 -0
  225. package/src/ssr/layout-help-start.ts +33 -7
  226. package/src/ssr/panel-consent.ts +29 -0
  227. package/src/ssr/panel-context.ts +10 -0
  228. package/src/ssr/panel-slot.browser-harness.tsx +159 -0
  229. package/src/ssr/platform-messages.ts +48 -0
  230. package/src/ssr/rail-navigation.ts +44 -0
  231. package/src/ssr/shell-data.tsx +5 -0
  232. package/src/ssr/shell-shortcuts.ts +13 -2
  233. package/src/styles/effects.css +201 -11
  234. package/src/styles/file-chooser.css +2 -1
  235. package/src/styles/global.css +314 -63
  236. package/src/styles/panel.css +416 -0
  237. package/src/styles/resource-search.css +6 -2
  238. package/src/styles/tokens.css +36 -13
  239. package/src/styles/utilities-buttons.css +3 -2
  240. package/src/styles/utilities-data.css +2 -1
  241. package/src/styles/utilities-feedback.css +2 -2
  242. package/src/styles/utilities-layout.css +3 -13
  243. package/src/styles/utilities-markdown-editor.css +6 -6
  244. package/src/styles/utilities-navigation.css +22 -43
@@ -2,26 +2,31 @@ import type { DateContext } from "@k2b/stdlib";
2
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"; // ==========================
8
+ import { type CloudResourceRef, CloudResourceRefSchema } from "../contracts/capabilities";
9
+ import { PANEL_HELP_ID_MAX_LENGTH, PanelPageContextSchema } from "../contracts/panel-context";
7
10
  import { isPwaShellAvailable } from "../contracts/pwa";
8
11
  import { defaultRailPreferences } from "../contracts/rail-preferences";
9
12
  import { hasRole, type User } from "../contracts/shared";
10
13
  import { getLocale } from "../server/locale";
11
14
  import { getDateConfig } from "../server/time";
12
- import { dates } from "../shared";
15
+ import { dates, resolveHomePath } from "../shared";
13
16
  import { readThemePreferenceFromCookieHeader } from "../shared/theme";
14
17
  import AnnouncementBanners from "./AnnouncementBanners";
15
18
  import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
16
- import { resolveRuntimeRoute, visibleNavigationApps } from "./app-navigation";
19
+ import { resolveRuntimeRoute, visibleNavigationApps, visiblePanelProviders } from "./app-navigation";
17
20
  import { getLayoutHelp, layoutHelpFailed } from "./help";
18
21
  import LayoutFooter from "./LayoutFooter";
19
22
  import LayoutHeader from "./LayoutHeader";
20
23
  import LayoutRail, { type LayoutAppLink } from "./LayoutRail";
24
+ import LayoutTabBar from "./LayoutTabBar";
21
25
  import type { LayoutContext } from "./layout-context";
22
26
  import { HELP_VIEW_LIMIT } from "./layout-help-start";
23
27
  import type { LayoutBreadcrumb } from "./layout-runtime";
24
28
  import PageTrailRow from "./PageTrailRow";
29
+ import PanelSlot from "./PanelSlot";
25
30
  import { derivePageTrail } from "./page-trail";
26
31
  import { platformMessages } from "./platform-messages";
27
32
  import { sortRailApps } from "./rail-navigation";
@@ -39,8 +44,12 @@ type LayoutProps = {
39
44
  title?: string | Breadcrumb[];
40
45
  /** 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. */
41
46
  help?: readonly string[];
47
+ /** Share only the current place with the visible panel; false disables context for this document. */
48
+ context?: { scope?: CloudResourceRef } | false;
42
49
  fullPage?: boolean /** Keep the shell viewport-bound and suppress its footer. */;
43
50
  fullWidth?: boolean /** Delegate scrolling and clipping to the page's work surface. */;
51
+ /** Hide the panel slot on this page (sign-in, print, presentation, public pages). */
52
+ panel?: false;
44
53
  focusMode?: boolean /** Render only the app canvas and content for dedicated editors or pop-out windows. */;
45
54
  flushCanvas?: boolean /** Remove app-canvas spacing for edge-to-edge focus surfaces such as pop-out windows. */;
46
55
  /** Final localized app-specific export or printing guidance, shown when no data-print-region exists. */
@@ -155,6 +164,11 @@ export default function Layout(props: LayoutProps) {
155
164
  const lang = getLocale(c);
156
165
  const t = platformMessages.resolve([lang]).t;
157
166
  const user = c.get("user");
167
+ const providers = user && props.panel !== false ? visiblePanelProviders(runtime.apps, user, lang) : [];
168
+ const panelState = readPanelCookie(
169
+ providers.length ? cookie : undefined,
170
+ providers.map((provider) => provider.id),
171
+ );
158
172
  const pathname = new URL(c.req.raw.url).pathname;
159
173
  const currentRoute = resolveRuntimeRoute(runtime.apps, pathname);
160
174
  const currentApp = resolveCurrentApp(runtime.apps, pathname);
@@ -185,6 +199,7 @@ export default function Layout(props: LayoutProps) {
185
199
  const page = c.get("page") as Record<string, unknown>;
186
200
  const pageTitle = typeof title === "string" ? title : (title?.at(-1)?.title ?? appName);
187
201
  if (!page.title) page.title = pageTitle;
202
+ page.appAccent = currentApp?.appearance?.accent;
188
203
  const breadcrumbs: Breadcrumb[] = !title ? [{ title: appName }] : typeof title === "string" ? [{ title }] : title;
189
204
  // The way up is derived from the title array by one rule (see `derivePageTrail`). Anonymous pages keep their
190
205
  // header breadcrumbs, and focus surfaces such as pop-out editors have no way up; both show the title alone. The
@@ -207,19 +222,48 @@ export default function Layout(props: LayoutProps) {
207
222
  // keeps short and long pages at the same width where scrollbars take space.
208
223
  const mainLayoutClass = fullPage || fullWidth ? "flex flex-col" : "md:overflow-auto md:[scrollbar-gutter:stable]";
209
224
  const canvasStyle =
210
- [appAppearanceStyle(currentApp?.appearance), appWorkspaceLayoutStyle(workspaceLayout), focusMode && flushCanvas ? "padding:0" : ""]
225
+ [
226
+ appAppearanceStyle(currentApp?.appearance),
227
+ appWorkspaceLayoutStyle(workspaceLayout),
228
+ panelState.widthRem !== undefined ? `--cloud-panel-width-set:${panelState.widthRem}rem` : "",
229
+ focusMode && flushCanvas ? "padding:0" : "",
230
+ ]
211
231
  .filter(Boolean)
212
232
  .join(";") || undefined;
213
233
  const appLabel = currentApp?.name ?? (pathname.startsWith("/admin") ? t.admin : undefined);
214
234
  const helpIds = new Set(registeredHelp?.documents.map((topic) => topic.id));
215
- const viewHelp = [...new Set(props.help)].filter((id) => helpIds.has(id)).slice(0, HELP_VIEW_LIMIT);
235
+ const viewHelp = [...new Set(props.help)]
236
+ .filter((id) => helpIds.has(id) && id.length <= PANEL_HELP_ID_MAX_LENGTH)
237
+ .slice(0, HELP_VIEW_LIMIT);
238
+ const scope = props.context && CloudResourceRefSchema.safeParse(props.context.scope);
239
+ // Fail closed: a page whose place does not validate shares nothing rather than failing to render.
240
+ const pageContext =
241
+ (providers.length &&
242
+ props.context !== false &&
243
+ PanelPageContextSchema.safeParse({
244
+ v: 1,
245
+ seq: 0,
246
+ pageId: crypto.randomUUID(),
247
+ app: currentApp?.id,
248
+ ...(scope && scope.success ? { scope: scope.data } : {}),
249
+ ...(viewHelp.length ? { help: viewHelp } : {}),
250
+ }).data) ||
251
+ null;
252
+ const panel: ShellData["panel"] = providers.length
253
+ ? { providers, provider: panelState.provider ?? providers[0]?.id, context: pageContext, consent: c.get("panelConsent") }
254
+ : undefined;
216
255
  const shellData: ShellData = {
256
+ ...(panel ? { panel } : {}),
217
257
  platform: keyboardPlatform(c.req.raw.headers.get("User-Agent") ?? ""),
218
258
  ...(viewHelp.length ? { help: viewHelp } : {}),
219
259
  ...(layoutHelpFailed(c) ? { helpFailed: true as const } : {}),
220
260
  apps: allApps,
221
261
  ...(user
222
- ? { rail: c.get("railPreferences") ?? defaultRailPreferences(), profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref } }
262
+ ? {
263
+ rail: c.get("railPreferences") ?? defaultRailPreferences(),
264
+ home: resolveHomePath(settings?.app?.home_path),
265
+ profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref },
266
+ }
223
267
  : {}),
224
268
  legalLinks,
225
269
  theme,
@@ -231,20 +275,27 @@ export default function Layout(props: LayoutProps) {
231
275
  return (
232
276
  <LocaleProvider locale={lang}>
233
277
  <div
234
- class="cloud-app-canvas flex h-dvh w-full overflow-hidden"
278
+ class="cloud-app-canvas relative flex h-dvh w-full overflow-hidden"
235
279
  style={canvasStyle}
280
+ data-panel-transient=""
236
281
  data-app-id={currentApp?.id}
237
282
  data-app-base={currentRoute?.prefix}
238
283
  data-page-trail-title-only=""
239
284
  data-layout-full-page="true"
240
285
  data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
241
286
  >
242
- <ShellRuntime pageTrail help={runtimeHelp} />
287
+ <ShellRuntime pageTrail help={runtimeHelp} panel={panel ? true : undefined} />
243
288
  <PageTrailProvider trail={pageTrail}>
244
- <main class="min-h-0 min-w-0 flex-1">
289
+ <main class="min-h-0 min-w-0 flex-1" data-cloud-app-area="">
245
290
  <AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
246
291
  </main>
247
292
  </PageTrailProvider>
293
+ {/* Focus pages carry shell data only for their panel providers. */}
294
+ {panel && (
295
+ <ShellDataProvider data={{ panel, platform: shellData.platform, theme, apps: [], legalLinks: [], searchLinks: {} }}>
296
+ <PanelSlot panel={panel} state={{ ...panelState, open: false }} platform={shellData.platform} />
297
+ </ShellDataProvider>
298
+ )}
248
299
  <div data-print-fallback="">
249
300
  <p>{t.printFallbackTitle}</p>
250
301
  <p>{props.printHint ?? t.printFallbackHint}</p>
@@ -261,6 +312,7 @@ export default function Layout(props: LayoutProps) {
261
312
  <div
262
313
  class={`cloud-app-canvas relative flex w-full ${fullPage ? "h-dvh overflow-hidden" : "min-h-dvh md:h-dvh md:overflow-hidden"}`}
263
314
  style={canvasStyle}
315
+ data-panel={panelState.open ? "open" : undefined}
264
316
  data-app-id={currentApp?.id}
265
317
  data-app-base={currentRoute?.prefix}
266
318
  data-page-trail-title-only={titleOnly ? "" : undefined}
@@ -270,6 +322,7 @@ export default function Layout(props: LayoutProps) {
270
322
  data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
271
323
  >
272
324
  <ShellRuntime
325
+ panel={panel ? true : undefined}
273
326
  workspace={{ appId: currentApp?.id }}
274
327
  pageTrail
275
328
  focus={showRail}
@@ -284,11 +337,12 @@ export default function Layout(props: LayoutProps) {
284
337
  banners={user && announcements?.banners.length ? true : undefined}
285
338
  />
286
339
  <ShellDataProvider data={shellData}>
287
- {showRail && <LayoutRail currentPath={requestPath(c)} />}
340
+ {showRail && <LayoutRail currentPath={requestPath(c)} panelOpen={panel ? panelState.open : undefined} />}
288
341
  {showRail && <ShellFocusToggle variant="float" />}
289
342
  {showRail && <div class="cloud-shell-reveal" data-cloud-shell-reveal="" aria-hidden="true" />}
290
- <div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col">
343
+ <div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col" data-cloud-app-area="">
291
344
  <LayoutHeader
345
+ panelOpen={panel ? panelState.open : undefined}
292
346
  authenticated={showRail}
293
347
  title={appLabel ?? breadcrumbs.at(-1)?.title}
294
348
  launchpad={!!(user || appLabel)}
@@ -305,6 +359,8 @@ export default function Layout(props: LayoutProps) {
305
359
  </PageTrailProvider>
306
360
  {!fullPage && !showRail && <LayoutFooter appName={settings?.app?.copyright || appName} legalLinks={legalLinks} />}
307
361
  </div>
362
+ {panel && <PanelSlot panel={panel} state={panelState} platform={shellData.platform} />}
363
+ {showRail && <LayoutTabBar currentPath={requestPath(c)} />}
308
364
  </ShellDataProvider>
309
365
  <div data-print-fallback="">
310
366
  <p>{t.printFallbackTitle}</p>
@@ -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
- * Anonymous pages have no rail: up to `lg` their header keeps the app name,
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.
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
- <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>
36
- </div>
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}
@@ -75,6 +75,8 @@ window.fetch = Object.assign(
75
75
  const ids = params.has("start")
76
76
  ? documents.filter((document) => document.title.toLocaleLowerCase().includes(query)).map((document) => document.id)
77
77
  : ["topic-0", "topic-1"];
78
+ // Tests wait for an answered search by this count, not for a spinner that can come and go between two polls (#1403).
79
+ document.documentElement.dataset.helpSearches = String(Number(document.documentElement.dataset.helpSearches ?? 0) + 1);
78
80
  return Response.json({ locale: "en", ids });
79
81
  }
80
82
  const id = path.split("/").at(-1) ?? "topic-0";