@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
@@ -7,7 +7,8 @@ import type { User } from "../contracts/shared";
7
7
  import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
8
8
  import { appAppearanceStyle } from "./app-appearance";
9
9
  import Layout from "./Layout";
10
- import { LayoutHelpPage, type LayoutHelpPageProps, LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelp";
10
+ import { LayoutHelpPage, type LayoutHelpPageProps } from "./LayoutHelp";
11
+ import { LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelpPanel.server";
11
12
  import { helpArticlePartsServer } from "./layout-help-article.server";
12
13
  import PanelLayout from "./PanelLayout";
13
14
  import { readPanelContext } from "./panel-context";
@@ -34,7 +35,8 @@ export const fixtureDocument: HelpDocumentPayload = {
34
35
  // A lead, the facts chips, and a section: everything the article head reads from the article.
35
36
  html: '<p>Share a Space so other people can see and edit its tasks.</p><h2 id="share">Share</h2><div class="help-steps"><ol><li>Open Space settings.</li><li>Add a person.</li></ol></div>',
36
37
  };
37
- const initialArticle = { payload: fixtureDocument, parts: helpArticlePartsServer(fixtureDocument.html) };
38
+ const { html, ...payload } = fixtureDocument;
39
+ const initialArticle = { payload, parts: helpArticlePartsServer(html) };
38
40
  const appearance: AppAppearance = { accent: "#16a34a", background: { from: "#16a34a", to: "#0284c7" } };
39
41
  const apps: CloudRuntime["apps"] = [
40
42
  {
@@ -65,7 +67,7 @@ const app = defineApp({
65
67
  routes: ["/"],
66
68
  });
67
69
  type Environment = { Variables: { runtime: CloudRuntime; user: User } };
68
- const PropsSnapshot = (props: { value: LayoutHelpPanelProps | LayoutHelpPageProps }) => (
70
+ const PropsSnapshot = (props: { value: LayoutHelpPageProps }) => (
69
71
  <script id="help-fixture-props" type="application/json" innerHTML={JSON.stringify(props.value).replace(/</g, "\\u003c")} />
70
72
  );
71
73
  export const server = new Hono<Environment>()
@@ -98,10 +100,7 @@ export const server = new Hono<Environment>()
98
100
  };
99
101
  return () => (
100
102
  <PanelLayout c={c} title="Help">
101
- <div id="help-fixture-root" class="flex h-full min-h-0 flex-1 flex-col">
102
- <LayoutHelpPanel {...props} />
103
- </div>
104
- <PropsSnapshot value={props} />
103
+ <LayoutHelpPanel {...props} />
105
104
  </PanelLayout>
106
105
  );
107
106
  }),
@@ -0,0 +1,131 @@
1
+ import { basename, resolve } from "node:path";
2
+ import tailwind from "bun-plugin-tailwind";
3
+ import { Hono } from "hono";
4
+ import { toJSON } from "seroval";
5
+ import { createPanelRenderer, panelFixtureProps, panelIslandEntries } from "../../../../scripts/fixtures/help-panel";
6
+ import { buildIslandClosure } from "../../../../scripts/fixtures/island-closure";
7
+ import { islandIdFromFile } from "../../../ssr/src/island-id";
8
+ import { helpArticlePartsServer } from "./layout-help-article.server";
9
+
10
+ /**
11
+ * Build the real islands together, with the production mount runtime and shared ESM chunks. One module script imports
12
+ * every island at once, as @k2b/ssr's loader does, so a held island does not hold the others.
13
+ */
14
+ export async function helpPanelServerBrowserFixture() {
15
+ const root = resolve(import.meta.dir, "../../../..");
16
+ const renderer = await createPanelRenderer();
17
+ const files: Record<string, string> = {};
18
+ const entrypoints = panelIslandEntries.map((entry) => {
19
+ const id = islandIdFromFile(entry, root);
20
+ const virtual = resolve(import.meta.dir, `${id}.ts`);
21
+ files[virtual] =
22
+ `import { mount } from ${JSON.stringify(resolve(root, "packages/ssr/src/mount.ts"))}; import C from ${JSON.stringify(entry)}; mount(C, 'solid-island[data-id="${id}"]'); document.documentElement.dataset[${JSON.stringify(`mounted${id}`)}] = "true";`;
23
+ return virtual;
24
+ });
25
+ try {
26
+ const build = await buildIslandClosure(entrypoints, files);
27
+ if (!build.metafile) throw new Error("Missing browser fixture metafile");
28
+ const outputMeta = new Map(Object.entries(build.metafile.outputs).map(([path, output]) => [basename(path), output]));
29
+ const eager = new Set<string>();
30
+ const visit = (name: string) => {
31
+ if (eager.has(name)) return;
32
+ eager.add(name);
33
+ for (const edge of outputMeta.get(name)?.imports ?? []) if (edge.kind !== "dynamic-import") visit(basename(edge.path));
34
+ };
35
+ const scripts = entrypoints.map((entry) => {
36
+ const output = [...outputMeta].find(([, meta]) => meta.entryPoint && resolve(meta.entryPoint) === entry);
37
+ if (!output) throw new Error(`Missing browser island entry ${entry}`);
38
+ visit(output[0]);
39
+ return output[0];
40
+ });
41
+ const outputs = new Map(await Promise.all(build.outputs.map(async (output) => [basename(output.path), await output.text()] as const)));
42
+ const dynamicChunks = [...outputMeta.keys()].filter((name) => !eager.has(name));
43
+ const chunkFor = (suffix: string) => {
44
+ const found = [...outputMeta]
45
+ .filter(([, meta]) => Object.entries(meta.inputs).some(([input, details]) => input.endsWith(suffix) && details.bytesInOutput > 0))
46
+ .map(([name]) => name);
47
+ if (!found.length) throw new Error(`Missing chunk for ${suffix}`);
48
+ return found;
49
+ };
50
+ const styles = await Bun.build({ entrypoints: [resolve(root, "styles.css")], plugins: [tailwind] });
51
+ if (!styles.success) throw new AggregateError(styles.logs, "Could not compile panel styles");
52
+ const css = await styles.outputs[0]!.text();
53
+ const initialArticle = {
54
+ payload: {
55
+ id: "share",
56
+ title: "Share a Space",
57
+ locale: "en",
58
+ kind: "task" as const,
59
+ needs: "edit" as const,
60
+ markdown: "Share a Space.\n\n## Invite\n\nInvite people.",
61
+ related: [],
62
+ },
63
+ parts: helpArticlePartsServer(
64
+ '<p>Share a Space.</p><h2 id="invite">Invite</h2><p>Invite people.</p><a href="/help/apps/spaces/roles">Roles</a>',
65
+ ),
66
+ };
67
+ const runtimeId = islandIdFromFile(panelIslandEntries.at(-1)!, root);
68
+ const runtimeProps = JSON.stringify(toJSON({ provider: "fixture", scope: "/panel" }))
69
+ .replaceAll("&", "&amp;")
70
+ .replaceAll('"', "&quot;");
71
+ const app = new Hono()
72
+ // Split chunks import each other by relative paths from the build's output layout; serve every file by name.
73
+ .get("/:path{.+\\.js}", (c) => {
74
+ const content = outputs.get(basename(c.req.path));
75
+ return content === undefined ? c.notFound() : c.body(content, 200, { "content-type": "text/javascript" });
76
+ })
77
+ .get("/", (c) =>
78
+ c.html(
79
+ `<!doctype html><html lang="en"${themeClass(c.req.query("theme"))}><head><style>${css}</style></head><body class="k2b-ui" style="margin:0"><div class="cloud-app-canvas" data-panel="open" data-panel-sheet="open" style="position:relative;display:flex;height:100dvh"><main data-cloud-app-area style="flex:1">Host app</main><aside class="cloud-panel" data-state="ready"><div class="cloud-panel__card"><header class="cloud-panel__head">Help</header><div class="cloud-panel__body"><iframe class="cloud-panel__frame" title="Help" src="/panel?${new URLSearchParams({ ...(c.req.query("topic") ? { topic: "share" } : {}), theme: c.req.query("theme") ?? "light" })}"></iframe></div></div></aside></div></body></html>`,
80
+ ),
81
+ )
82
+ .get("/panel", (c) =>
83
+ c.html(
84
+ `<!doctype html><html lang="en"${themeClass(c.req.query("theme"))}><head><style>${css}</style><style>html,body{margin:0;height:100%}*,*::before,*::after{animation:none!important;transition:none!important}</style></head><body class="k2b-ui"><div class="cloud-panel-page" data-cloud-panel-page><script id="cloud-panel-context" type="application/json">{"v":1,"seq":0,"pageId":"22222222-2222-4222-8222-222222222222","app":"spaces","help":["share"]}</script><solid-island data-id="${runtimeId}" data-props="${runtimeProps}"></solid-island>${renderer.render({ ...panelFixtureProps, ...(c.req.query("topic") ? { initialTopic: "share", initialArticle } : {}) })}</div><script>document.documentElement.dataset.serverGeometry=JSON.stringify((${panelGeometry.toString()})());</script><script type="module">${JSON.stringify(scripts)}.forEach((file)=>import("/assets/"+file))</script></body></html>`,
85
+ ),
86
+ )
87
+ .get("/api/help/search", (c) => c.json({ ids: ["share"] }))
88
+ .get("/api/help/:id", (c) =>
89
+ c.json({
90
+ ...initialArticle.payload,
91
+ id: c.req.param("id"),
92
+ html: '<p>Fixture.</p><h2 id="invite">Invite</h2><p>Invite people.</p>',
93
+ }),
94
+ );
95
+ const server = Bun.serve({ port: 0, fetch: app.fetch });
96
+ return {
97
+ origin: `http://localhost:${server.port}`,
98
+ dynamicChunks,
99
+ searchChunks: chunkFor("/layout-help-panel-search.tsx"),
100
+ menuChunks: chunkFor("/layout-help-panel-menu.tsx"),
101
+ islandChunks: new Map(panelIslandEntries.map((entry, index) => [basename(entry), scripts[index]!])),
102
+ ids: panelIslandEntries.map((entry) => islandIdFromFile(entry, root)),
103
+ cleanup: () => {
104
+ server.stop(true);
105
+ renderer.cleanup();
106
+ },
107
+ };
108
+ } catch (error) {
109
+ renderer.cleanup();
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /** Cloud's theme script sets `dark` on the root; the fixture renders that class on the server for both documents. */
115
+ const themeClass = (theme: string | undefined) => (theme === "dark" ? ' class="dark"' : "");
116
+
117
+ /** First-frame geometry is the assertion subject, so callers also take it while action chunks are held. */
118
+ export function panelGeometry() {
119
+ return [
120
+ "[data-help-head]",
121
+ "[data-help-search]",
122
+ "[data-help-results]",
123
+ "[data-help-tiles] button",
124
+ "[data-help-server-article] [data-help-article-head]",
125
+ ].map((selector) => {
126
+ const element = document.querySelector<HTMLElement>(selector);
127
+ if (!element || element.closest("[hidden]")) return null;
128
+ const rect = element.getBoundingClientRect();
129
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
130
+ });
131
+ }
@@ -0,0 +1,115 @@
1
+ import { MarkdownView, ScrollArea, TableOfContents, useLocale } from "@k2b/ui";
2
+ import { Show } from "solid-js";
3
+ import HelpPanelArticle from "./HelpPanelArticle.island";
4
+ import HelpPanelBack from "./HelpPanelBack.island";
5
+ import HelpPanelMenu from "./HelpPanelMenu.island";
6
+ import HelpPanelSearch from "./HelpPanelSearch.island";
7
+ import HelpPanelShortcuts from "./HelpPanelShortcuts.island";
8
+ import HelpPanelView from "./HelpPanelView.island";
9
+ import { helpMessages } from "./help-messages";
10
+ import { HelpPanelSectionTrigger } from "./layout-help-panel-controls";
11
+ import type { HelpPanelData } from "./layout-help-panel-state";
12
+ import type { LayoutHelpPanelProps } from "./layout-help-panel-types";
13
+ import { helpStart } from "./layout-help-start";
14
+ import { HelpArticleView, HelpHead, HelpNotAvailableView, HelpStartView } from "./layout-help-views";
15
+
16
+ export type { LayoutHelpPanelProps } from "./layout-help-panel-types";
17
+
18
+ /** Static provider content survives island mounting; only the controls own browser state. */
19
+ export function LayoutHelpPanel(props: LayoutHelpPanelProps) {
20
+ const locale = useLocale();
21
+ const t = () => helpMessages.resolve([locale()]).t;
22
+ const topic = props.documents.find((topic) => topic.id === props.initialTopic);
23
+ const article = topic && props.initialArticle?.payload.id === topic.id ? props.initialArticle : undefined;
24
+ const start = helpStart(props.documents, props.view);
25
+ const data: HelpPanelData = {
26
+ // The legacy reader searched and copied its topics in start order; keep that order for every manifest.
27
+ documents: [...props.documents]
28
+ .sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title))
29
+ .map(({ id, title, description, icon, kind, order, url, searchUrl, locale }) => ({
30
+ id,
31
+ title,
32
+ description,
33
+ icon,
34
+ kind,
35
+ order,
36
+ url,
37
+ searchUrl,
38
+ locale,
39
+ })),
40
+ appId: props.app?.id,
41
+ pageBase: props.pageBase,
42
+ article: article?.payload,
43
+ };
44
+ return (
45
+ <div
46
+ class="help-shell app-accent-scope flex h-full min-h-0 flex-col"
47
+ data-surface="panel"
48
+ style={props.appearance}
49
+ data-help-initial-topic={topic?.id}
50
+ >
51
+ <Show when={props.state !== "unavailable"}>
52
+ <HelpHead
53
+ row
54
+ back={!!topic}
55
+ tile={props.app?.icon ?? "ti ti-help-circle"}
56
+ title={props.app?.name ?? t().help}
57
+ lead={<HelpPanelBack back={!!topic} tile={props.app?.icon ?? "ti ti-help-circle"} />}
58
+ menu={<HelpPanelMenu />}
59
+ closable={false}
60
+ onBack={() => {}}
61
+ onClose={() => {}}
62
+ query=""
63
+ onQuery={() => {}}
64
+ searchRef={() => {}}
65
+ />
66
+ </Show>
67
+ <ScrollArea class="min-h-0 flex-1 p-4 sm:p-5">
68
+ <Show when={props.state !== "unavailable"} fallback={<HelpNotAvailableView />}>
69
+ <div class="help-start help-measure" data-help-results="" hidden={!!topic}>
70
+ <div data-help-start-content="">
71
+ <HelpPanelView topics={start.view} />
72
+ <HelpStartView
73
+ start={{ ...start, view: [] }}
74
+ empty={props.documents.length === 0}
75
+ failed={props.state === "failed"}
76
+ onOpen={() => {}}
77
+ onReload={() => {}}
78
+ shortcuts={<HelpPanelShortcuts appName={props.app?.name} platform={props.platform} />}
79
+ />
80
+ </div>
81
+ <HelpPanelSearch />
82
+ </div>
83
+ <Show when={article && topic}>
84
+ {article && topic && (
85
+ <article class="help-article" data-surface="panel" data-help-server-article="">
86
+ <HelpArticleView
87
+ surface="panel"
88
+ topic={topic}
89
+ appName={props.app?.name}
90
+ appId={props.app?.id}
91
+ parts={article.parts}
92
+ document={{ ...article.payload, html: "" }}
93
+ Markdown={MarkdownView}
94
+ showTools={article.parts.sections.length > 0}
95
+ sectionMenu={article.parts.sections.length ? <HelpPanelSectionTrigger label={t().onThisPage} /> : null}
96
+ copyButton={null}
97
+ loading={null}
98
+ failure={null}
99
+ toc={<TableOfContents items={article.parts.sections} title={t().onThisPage} />}
100
+ copyAction={null}
101
+ headingRef={() => {}}
102
+ contentRef={() => {}}
103
+ onBack={() => {}}
104
+ />
105
+ </article>
106
+ )}
107
+ </Show>
108
+ <HelpPanelArticle topic={article ? undefined : topic} />
109
+ </Show>
110
+ <span class="sr-only" aria-live="polite" data-help-announcement="" />
111
+ </ScrollArea>
112
+ <script type="application/json" data-help-panel-data="" innerHTML={JSON.stringify(data).replace(/</g, "\\u003c")} />
113
+ </div>
114
+ );
115
+ }
@@ -1,6 +1,7 @@
1
1
  import { installAppWorkspaceController, installInputModeController, keyboardPlatform } from "@k2b/ui";
2
2
  import { onCleanup, onMount } from "solid-js";
3
3
  import { installPanelContextReceiver } from "../browser/panel-context-receiver";
4
+ import { hasPanelLeaveGuard, panelLinkDestination } from "../browser/panel-leave";
4
5
  import { syncTimezoneCookie } from "./timezone-cookie";
5
6
 
6
7
  export default function PanelRuntime(props: { provider?: string; scope?: string }) {
@@ -24,15 +25,18 @@ export default function PanelRuntime(props: { provider?: string; scope?: string
24
25
  };
25
26
  const close = () => window.parent.postMessage({ type: "cloud.panel.close" }, location.origin);
26
27
  const click = (event: MouseEvent) => {
27
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
28
- const anchor = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>("a[href]") : null;
29
- if (!anchor || (anchor.target && anchor.target !== "_self") || anchor.hasAttribute("download")) return;
30
- const url = new URL(anchor.getAttribute("href")!, document.baseURI);
31
- if (url.origin !== location.origin) return;
28
+ const url = panelLinkDestination(event);
29
+ if (!url) return;
32
30
  const scope = props.scope?.replace(/\/$/, "");
33
- if (scope !== undefined && (url.pathname === scope || url.pathname.startsWith(`${scope}/`))) return;
31
+ const internal = scope !== undefined && (url.pathname === scope || url.pathname.startsWith(`${scope}/`));
32
+ if (!hasPanelLeaveGuard()) {
33
+ if (internal) return;
34
+ event.preventDefault();
35
+ window.parent.location.assign(url.href);
36
+ return;
37
+ }
34
38
  event.preventDefault();
35
- window.parent.location.assign(url.href);
39
+ window.parent.postMessage({ type: "cloud.panel.navigate", v: 1, href: url.href }, location.origin);
36
40
  };
37
41
  const key = (event: KeyboardEvent) => {
38
42
  if (event.isComposing || event.defaultPrevented || event.altKey || event.shiftKey) return;
@@ -6,7 +6,7 @@ import { visiblePwaParts } from "./app-navigation";
6
6
  import type { LayoutContext } from "./layout-context";
7
7
  import PwaRuntime from "./PwaRuntime.island";
8
8
  import { pwaMessages } from "./pwa-messages";
9
- import { getRuntimeContext } from "./runtime";
9
+ import { getDisplayRuntimeContext } from "./runtime";
10
10
  import TimezoneCookie from "./TimezoneCookie.island";
11
11
 
12
12
  /** Parts with their own tab; Start lists every part and doubles as "More". */
@@ -42,7 +42,7 @@ export default function PwaLayout(props: PwaLayoutProps) {
42
42
  c.header("Referrer-Policy", "no-referrer");
43
43
  c.header("Content-Security-Policy", "frame-ancestors 'none'");
44
44
 
45
- const parts = visiblePwaParts(getRuntimeContext(c).apps, user, locale);
45
+ const parts = visiblePwaParts(getDisplayRuntimeContext(c).apps, user, locale);
46
46
  const path = new URL(c.req.raw.url).pathname;
47
47
  const tabs = parts.slice(0, TAB_PARTS);
48
48
  const currentTab = tabs.find((part) => path === part.pwa.href || path.startsWith(`${part.pwa.href}/`));
@@ -44,11 +44,17 @@ export default function RailApps(props: { apps: RailApp[]; settings: RailSnapsho
44
44
  const stopBadges = watchAppBadges(props.apps.flatMap((app) => (app.badge ? [app.badge] : [])));
45
45
  window.addEventListener(RAIL_PREFERENCES_EVENT, sync);
46
46
  window.addEventListener("popstate", syncLocation);
47
+ window.addEventListener("ssr:nav-popstate", syncLocation);
48
+ window.addEventListener("ssr:nav-swapped", sync);
49
+ window.addEventListener("ssr:nav-swapped", syncLocation);
47
50
  window.addEventListener("hashchange", syncLocation);
48
51
  onCleanup(() => {
49
52
  stopBadges();
50
53
  window.removeEventListener(RAIL_PREFERENCES_EVENT, sync);
51
54
  window.removeEventListener("popstate", syncLocation);
55
+ window.removeEventListener("ssr:nav-popstate", syncLocation);
56
+ window.removeEventListener("ssr:nav-swapped", sync);
57
+ window.removeEventListener("ssr:nav-swapped", syncLocation);
52
58
  window.removeEventListener("hashchange", syncLocation);
53
59
  });
54
60
  });
@@ -1,4 +1,5 @@
1
1
  import type { HelpDocumentManifest } from "@k2b/cloud/shared";
2
+ import { installPageSwap } from "@k2b/ssr/nav";
2
3
  import {
3
4
  followPageTrail,
4
5
  installAppWorkspaceController,
@@ -9,6 +10,7 @@ import {
9
10
  useLocale,
10
11
  } from "@k2b/ui";
11
12
  import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
13
+ import { isServer } from "solid-js/web";
12
14
  import { appWorkspaceCookieName, readAppWorkspaceLayoutCookie } from "../_internal/app-workspace-state";
13
15
  import { registerContextAwareCommand } from "../browser/command-bridge";
14
16
  import { attachCommandShortcuts } from "../browser/command-shortcuts";
@@ -58,10 +60,13 @@ export type ShellRuntimeProps = {
58
60
  const searchItems = () => {
59
61
  const data = readShellData();
60
62
  if (!data) return [];
61
- const apps = Object.entries(data.searchLinks).flatMap(([id, searchLinks]) => {
62
- const app = data.apps.find((candidate) => candidate.id === id);
63
- return app ? [{ id, name: app.label, icon: app.iconClass, searchLinks }] : [];
64
- });
63
+ const apps = data.apps.map((app) => ({
64
+ id: app.id,
65
+ name: app.label,
66
+ icon: app.iconClass,
67
+ nav: { href: app.href },
68
+ searchLinks: data.searchLinks[app.id],
69
+ }));
65
70
  return navigationSearchItems(
66
71
  apps,
67
72
  apps.map((app) => app.id),
@@ -69,6 +74,7 @@ const searchItems = () => {
69
74
  };
70
75
 
71
76
  export default function ShellRuntime(props: ShellRuntimeProps) {
77
+ if (!isServer && document.querySelector(".cloud-app-canvas[data-nav-shell]")) onCleanup(installPageSwap());
72
78
  const locale = useLocale();
73
79
  const t = () => platformMessages.resolve([locale()]).t;
74
80
  onMount(() => syncTimezoneCookie(true));
@@ -132,7 +138,7 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
132
138
  if (props.workspace) {
133
139
  onMount(() => {
134
140
  const appId = props.workspace?.appId;
135
- onCleanup(
141
+ const install = () =>
136
142
  installAppWorkspaceController({
137
143
  readState: () => readAppWorkspaceLayoutCookie(document.cookie, appId),
138
144
  writeState: (state) => {
@@ -140,8 +146,18 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
140
146
  const secure = window.location.protocol === "https:" ? "; Secure" : "";
141
147
  document.cookie = `${appWorkspaceCookieName(appId)}=${serializeAppWorkspaceLayoutState(state)}; Path=/; Max-Age=31536000; SameSite=Lax${secure}`;
142
148
  },
143
- }),
144
- );
149
+ });
150
+ let dispose = install();
151
+ // The controller reads the saved layout once; a swapped page may carry sizes another tab saved since.
152
+ const swapped = () => {
153
+ dispose();
154
+ dispose = install();
155
+ };
156
+ window.addEventListener("ssr:nav-swapped", swapped);
157
+ onCleanup(() => {
158
+ window.removeEventListener("ssr:nav-swapped", swapped);
159
+ dispose();
160
+ });
145
161
  });
146
162
  }
147
163
  if (props.pageTrail) {