@k2b/cloud 0.39.0 → 0.41.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 (237) hide show
  1. package/README.md +8 -0
  2. package/package.json +11 -4
  3. package/src/_internal/app-favicon.ts +4 -2
  4. package/src/_internal/capabilities.ts +72 -48
  5. package/src/_internal/capability-reviews.ts +81 -0
  6. package/src/_internal/capability-schemas.ts +29 -0
  7. package/src/_internal/capability-sentences.ts +217 -1
  8. package/src/_internal/capability-streams.ts +7 -0
  9. package/src/_internal/define-app.ts +49 -18
  10. package/src/_internal/font-preloads.ts +2 -1
  11. package/src/_internal/heartbeat.ts +203 -14
  12. package/src/_internal/registry.ts +28 -1
  13. package/src/_internal/runtime-watcher.ts +6 -0
  14. package/src/_internal/setup-lock.ts +103 -0
  15. package/src/_internal/static-assets.ts +52 -3
  16. package/src/_internal/status-preserving-ssr.ts +2 -0
  17. package/src/access/PermissionEditor.tsx +2 -2
  18. package/src/account/EntitySearch.tsx +1 -1
  19. package/src/ai/action-review.ts +101 -0
  20. package/src/ai/background-review.ts +72 -0
  21. package/src/ai/capabilities.ts +29 -11
  22. package/src/ai/capability-execution.ts +76 -43
  23. package/src/ai/capability-result.ts +95 -0
  24. package/src/ai/chat/action-card.ts +464 -0
  25. package/src/ai/chat/action-messages.ts +129 -0
  26. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  27. package/src/ai/chat/action-question.fixtures.ts +733 -0
  28. package/src/ai/chat/action-question.tsx +680 -0
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/blocks.tsx +19 -25
  31. package/src/ai/chat/message-actions.tsx +6 -0
  32. package/src/ai/chat/tool-action-words.ts +31 -0
  33. package/src/ai/chat/tool-action.tsx +456 -0
  34. package/src/ai/chat/turn-view.tsx +10 -160
  35. package/src/ai/chat/view-block.tsx +15 -5
  36. package/src/ai/chat-task-contracts.ts +4 -1
  37. package/src/ai/chat-task-reasons-migrate.ts +7 -0
  38. package/src/ai/chat-task-reasons.ts +1 -1
  39. package/src/ai/chat-tasks.ts +55 -25
  40. package/src/ai/client/controller.ts +59 -0
  41. package/src/ai/default-tools.ts +9 -1
  42. package/src/ai/executor.ts +198 -39
  43. package/src/ai/firecrawl-tools.ts +15 -2
  44. package/src/ai/index.ts +3 -0
  45. package/src/ai/migrate.ts +26 -2
  46. package/src/ai/open-questions.ts +172 -0
  47. package/src/ai/projects-routes.ts +3 -0
  48. package/src/ai/protocol.ts +14 -2
  49. package/src/ai/question-text.ts +8 -0
  50. package/src/ai/recent-resources.ts +240 -0
  51. package/src/ai/recent-sent-refs.ts +38 -0
  52. package/src/ai/routes.ts +195 -5
  53. package/src/ai/runtime.ts +1 -0
  54. package/src/ai/skills-routes.ts +3 -0
  55. package/src/ai/skills.ts +10 -1
  56. package/src/ai/store.ts +245 -24
  57. package/src/ai/testing.ts +36 -0
  58. package/src/ai/tool-audit.ts +8 -0
  59. package/src/ai/tool-sentences.ts +152 -34
  60. package/src/ai/tool-wording.ts +36 -8
  61. package/src/ai/tools.ts +8 -3
  62. package/src/ai/types.ts +43 -4
  63. package/src/api/accounts-entities.ts +2 -1
  64. package/src/api/admin-ai-projects.ts +2 -0
  65. package/src/api/admin-ai-skills.ts +2 -0
  66. package/src/api/admin-ai-usage.ts +2 -0
  67. package/src/api/admin-geoip.ts +80 -0
  68. package/src/api/admin-identity.ts +3 -0
  69. package/src/api/admin-lifecycle.ts +3 -0
  70. package/src/api/admin-outgoing-mail.ts +2 -0
  71. package/src/api/app-approval-actions.ts +31 -7
  72. package/src/api/app-approval.ts +46 -5
  73. package/src/api/approvals-loader.ts +23 -1
  74. package/src/api/approvals.ts +2 -0
  75. package/src/api/auth/schemas.ts +3 -0
  76. package/src/api/auth.ts +67 -12
  77. package/src/api/capabilities.ts +44 -11
  78. package/src/api/capability-streams.ts +12 -0
  79. package/src/api/help.ts +4 -28
  80. package/src/api/index.ts +6 -1
  81. package/src/api/lasting-access-messages.ts +17 -0
  82. package/src/api/mcp.ts +30 -3
  83. package/src/api/me-panel-consent.ts +71 -0
  84. package/src/api/me.ts +159 -36
  85. package/src/api/search.ts +3 -1
  86. package/src/api/sync-ops.ts +2 -0
  87. package/src/api/widgets.ts +3 -0
  88. package/src/approvals/Approvals.tsx +166 -262
  89. package/src/approvals/approval-card.ts +68 -0
  90. package/src/approvals/messages.ts +12 -24
  91. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  92. package/src/browser/app-approval.ts +54 -5
  93. package/src/browser/commands.ts +22 -6
  94. package/src/browser/composer-catalog.ts +45 -3
  95. package/src/browser/composer-insert.ts +10 -0
  96. package/src/browser/composer-open-messages.ts +17 -0
  97. package/src/browser/composer-open.ts +110 -0
  98. package/src/browser/discoverable-command.ts +6 -0
  99. package/src/browser/hints.ts +6 -0
  100. package/src/browser/live-connection.ts +26 -2
  101. package/src/browser/navigation-apps.ts +7 -0
  102. package/src/browser/panel-context-bridge.ts +30 -0
  103. package/src/browser/panel-context-receiver.ts +53 -0
  104. package/src/browser/panel-menu.tsx +56 -0
  105. package/src/browser/panel-messages.ts +11 -0
  106. package/src/browser/panel-shortcuts.ts +31 -0
  107. package/src/browser/panel-slot.ts +412 -10
  108. package/src/browser/panel.ts +37 -0
  109. package/src/browser/search-commands.ts +6 -3
  110. package/src/capabilities/client.ts +8 -2
  111. package/src/capabilities/reviews.ts +205 -0
  112. package/src/capabilities/server.ts +10 -3
  113. package/src/capabilities/testing.ts +47 -0
  114. package/src/capabilities/types.ts +2 -2
  115. package/src/cli/account.ts +13 -3
  116. package/src/cli/admin/geoip.ts +115 -0
  117. package/src/cli/admin/index.ts +3 -0
  118. package/src/contracts/app-approval.ts +123 -16
  119. package/src/contracts/approvals.ts +2 -2
  120. package/src/contracts/capabilities.ts +410 -18
  121. package/src/contracts/geoip.ts +42 -0
  122. package/src/contracts/index.ts +3 -0
  123. package/src/contracts/panel-context-plain.ts +99 -0
  124. package/src/contracts/panel-context.ts +93 -0
  125. package/src/contracts/seen-hints.ts +35 -0
  126. package/src/contracts/shared.ts +3 -0
  127. package/src/server/client-ip.ts +5 -0
  128. package/src/server/help-payload.ts +38 -0
  129. package/src/server/middleware/auth.ts +13 -1
  130. package/src/server/middleware/middleware.ts +9 -0
  131. package/src/server/middleware/no-transform.ts +19 -0
  132. package/src/server/middleware/rate-limit.ts +4 -6
  133. package/src/server/request-context/address.ts +45 -0
  134. package/src/server/request-context/geoip.ts +380 -0
  135. package/src/server/request-context/index.ts +33 -0
  136. package/src/server/request-context/user-agent.ts +95 -0
  137. package/src/services/accounts/app.ts +23 -3
  138. package/src/services/accounts/users.ts +12 -1
  139. package/src/services/app-approval.ts +382 -59
  140. package/src/services/approvals/device.ts +49 -31
  141. package/src/services/approvals/index.ts +7 -5
  142. package/src/services/approvals/list.ts +27 -6
  143. package/src/services/approvals/question.ts +68 -0
  144. package/src/services/audit/index.ts +1 -0
  145. package/src/services/auth-flows/ipa.ts +39 -2
  146. package/src/services/auth-flows/notification-sender.ts +3 -0
  147. package/src/services/auth-flows/password-reset.ts +32 -6
  148. package/src/services/geoip/index.ts +164 -0
  149. package/src/services/geoip/store.ts +30 -0
  150. package/src/services/geoip/upload.ts +150 -0
  151. package/src/services/identity/routes.ts +5 -1
  152. package/src/services/index.ts +13 -0
  153. package/src/services/known-browsers.ts +312 -0
  154. package/src/services/login-address-store.ts +51 -0
  155. package/src/services/login-context.ts +118 -0
  156. package/src/services/mandates/index.ts +3 -0
  157. package/src/services/mandates/policy.ts +15 -3
  158. package/src/services/notifications/email-frame.ts +1 -1
  159. package/src/services/oauth-tokens.ts +16 -2
  160. package/src/services/panel-context-consent.ts +58 -0
  161. package/src/services/pdf/markdown.ts +160 -2
  162. package/src/services/service-account-credentials.ts +21 -0
  163. package/src/services/session/index.ts +296 -41
  164. package/src/services/session/recent.ts +13 -0
  165. package/src/services/webauthn.ts +12 -0
  166. package/src/shared/email-html.ts +2 -0
  167. package/src/shared/help.ts +3 -0
  168. package/src/shared-text/cursor.ts +22 -0
  169. package/src/shared-text/documents.ts +142 -0
  170. package/src/shared-text/events.ts +43 -0
  171. package/src/shared-text/index.ts +12 -0
  172. package/src/shared-text/log.ts +48 -0
  173. package/src/shared-text/text-diff.ts +23 -0
  174. package/src/shared-text/types.ts +41 -0
  175. package/src/ssr/AdminLayout.tsx +2 -0
  176. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  177. package/src/ssr/CloudShell.island.tsx +10 -2
  178. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  179. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  180. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  181. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  182. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  183. package/src/ssr/HelpPanelView.island.tsx +27 -0
  184. package/src/ssr/Layout.tsx +25 -2
  185. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  186. package/src/ssr/LayoutHelp.tsx +222 -419
  187. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
  188. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  189. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  190. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  191. package/src/ssr/MobileNavigation.tsx +2 -2
  192. package/src/ssr/PanelLayout.tsx +9 -1
  193. package/src/ssr/PanelRuntime.island.tsx +7 -0
  194. package/src/ssr/PanelSlot.tsx +53 -3
  195. package/src/ssr/ShellRuntime.island.tsx +11 -2
  196. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  197. package/src/ssr/app-color.ts +14 -9
  198. package/src/ssr/app-navigation.ts +16 -3
  199. package/src/ssr/compact-shell.browser-harness.ts +30 -22
  200. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  201. package/src/ssr/help-messages.ts +4 -0
  202. package/src/ssr/help.ts +21 -0
  203. package/src/ssr/hints.ts +25 -0
  204. package/src/ssr/index.ts +3 -0
  205. package/src/ssr/layout-context.ts +2 -0
  206. package/src/ssr/layout-help-article.server.ts +50 -0
  207. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  208. package/src/ssr/layout-help-panel-document.ts +67 -0
  209. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  210. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  211. package/src/ssr/layout-help-panel-modules.ts +7 -0
  212. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  213. package/src/ssr/layout-help-panel-search.tsx +69 -0
  214. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  215. package/src/ssr/layout-help-panel-state.ts +141 -0
  216. package/src/ssr/layout-help-panel-types.ts +19 -0
  217. package/src/ssr/layout-help-start.ts +33 -7
  218. package/src/ssr/layout-help-views.tsx +484 -0
  219. package/src/ssr/panel-consent.ts +29 -0
  220. package/src/ssr/panel-context.ts +10 -0
  221. package/src/ssr/panel-idle.ts +9 -0
  222. package/src/ssr/panel-slot.browser-harness.tsx +53 -4
  223. package/src/ssr/platform-messages.ts +18 -0
  224. package/src/ssr/shell-data.tsx +2 -1
  225. package/src/styles/approvals.css +6 -0
  226. package/src/styles/effects.css +213 -9
  227. package/src/styles/file-chooser.css +2 -1
  228. package/src/styles/global.css +49 -12
  229. package/src/styles/panel.css +47 -4
  230. package/src/styles/resource-search.css +6 -2
  231. package/src/styles/tokens.css +17 -11
  232. package/src/styles/utilities-buttons.css +3 -2
  233. package/src/styles/utilities-data.css +2 -1
  234. package/src/styles/utilities-feedback.css +2 -2
  235. package/src/styles/utilities-layout.css +3 -13
  236. package/src/styles/utilities-markdown-editor.css +6 -6
  237. package/src/styles/utilities-navigation.css +17 -22
@@ -0,0 +1,135 @@
1
+ import { keyboardPlatform } from "@k2b/ui";
2
+ import { Hono } from "hono";
3
+ import { defineApp } from "../_internal/define-app";
4
+ import { person } from "../_internal/define-app.test-fixture";
5
+ import type { AppAppearance, CloudRuntime } from "../contracts/app";
6
+ import type { User } from "../contracts/shared";
7
+ import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
8
+ import { appAppearanceStyle } from "./app-appearance";
9
+ import Layout from "./Layout";
10
+ import { LayoutHelpPage, type LayoutHelpPageProps } from "./LayoutHelp";
11
+ import { LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelpPanel.server";
12
+ import { helpArticlePartsServer } from "./layout-help-article.server";
13
+ import PanelLayout from "./PanelLayout";
14
+ import { readPanelContext } from "./panel-context";
15
+
16
+ export const documents: HelpDocumentManifest[] = [
17
+ { id: "spaces-share", title: "Share a Space", kind: "task", icon: "ti ti-share" },
18
+ { id: "spaces-start", title: "Start with Spaces", kind: "start", icon: "ti ti-rocket" },
19
+ ].map((topic) => ({
20
+ ...topic,
21
+ kind: topic.id === "spaces-share" ? "task" : "start",
22
+ order: 1,
23
+ url: `/api/help/${topic.id}`,
24
+ searchUrl: "/api/help/search",
25
+ }));
26
+ export const helpManifest = { documents, pageBase: "/help/apps/spaces", baseLocale: "en", locale: "en", manifestHash: "fixture" };
27
+ export const fixtureDocument: HelpDocumentPayload = {
28
+ id: "spaces-share",
29
+ title: "Share a Space",
30
+ locale: "en",
31
+ markdown: "Share",
32
+ kind: "task",
33
+ needs: "manage",
34
+ related: [],
35
+ // A lead, the facts chips, and a section: everything the article head reads from the article.
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>',
37
+ };
38
+ const { html, ...payload } = fixtureDocument;
39
+ const initialArticle = { payload, parts: helpArticlePartsServer(html) };
40
+ const appearance: AppAppearance = { accent: "#16a34a", background: { from: "#16a34a", to: "#0284c7" } };
41
+ const apps: CloudRuntime["apps"] = [
42
+ {
43
+ id: "spaces",
44
+ name: "Spaces",
45
+ icon: "ti ti-box",
46
+ description: "Help fixture",
47
+ routes: ["/app/spaces"],
48
+ nav: { href: "/app/spaces", section: "primary" },
49
+ appearance,
50
+ help: helpManifest,
51
+ },
52
+ {
53
+ id: "core",
54
+ name: "Core",
55
+ icon: "ti ti-cloud",
56
+ description: "Core fixture",
57
+ routes: ["/"],
58
+ panel: { href: "/panel", label: "Help", icon: "ti ti-help-circle" },
59
+ },
60
+ ];
61
+ const app = defineApp({
62
+ id: "help-fixture",
63
+ name: "Help fixture",
64
+ description: "Help fixture",
65
+ icon: "ti ti-help-circle",
66
+ baseUrl: "http://fixture:3000",
67
+ routes: ["/"],
68
+ });
69
+ type Environment = { Variables: { runtime: CloudRuntime; user: User } };
70
+ const PropsSnapshot = (props: { value: LayoutHelpPageProps }) => (
71
+ <script id="help-fixture-props" type="application/json" innerHTML={JSON.stringify(props.value).replace(/</g, "\\u003c")} />
72
+ );
73
+ export const server = new Hono<Environment>()
74
+ .use(async (c, next) => {
75
+ c.set("runtime", { apps });
76
+ c.set("user", person);
77
+ await next();
78
+ })
79
+ .get(
80
+ "/app/spaces",
81
+ ...app.ssr<Environment>((c) => () => (
82
+ <Layout c={c} title="Spaces" help={["spaces-share"]}>
83
+ <p>App content</p>
84
+ </Layout>
85
+ )),
86
+ )
87
+ .get(
88
+ "/panel",
89
+ ...app.ssr<Environment>((c) => {
90
+ const context = readPanelContext(c);
91
+ const props: LayoutHelpPanelProps = {
92
+ documents,
93
+ app: { id: "spaces", name: "Spaces", icon: "ti ti-box" },
94
+ appearance: appAppearanceStyle(appearance),
95
+ view: context?.app === "spaces" ? context.help?.filter((id) => documents.some((topic) => topic.id === id)) : [],
96
+ platform: keyboardPlatform(c.req.header("User-Agent") ?? ""),
97
+ pageBase: helpManifest.pageBase,
98
+ initialArticle: c.req.query("topic") === "spaces-share" ? initialArticle : undefined,
99
+ initialTopic: c.req.query("topic") === "spaces-share" ? "spaces-share" : undefined,
100
+ };
101
+ return () => (
102
+ <PanelLayout c={c} title="Help">
103
+ <LayoutHelpPanel {...props} />
104
+ </PanelLayout>
105
+ );
106
+ }),
107
+ )
108
+ .get(
109
+ "/help/apps/spaces/spaces-share",
110
+ ...app.ssr<Environment>((c) => () => (
111
+ <Layout c={c} title="Help">
112
+ <div id="help-fixture-root">
113
+ <LayoutHelpPage
114
+ documents={documents}
115
+ initialTopic="spaces-share"
116
+ initialArticle={initialArticle}
117
+ pageBase={helpManifest.pageBase}
118
+ />
119
+ </div>
120
+ <PropsSnapshot value={{ documents, initialTopic: "spaces-share", initialArticle, pageBase: helpManifest.pageBase }} />
121
+ </Layout>
122
+ )),
123
+ );
124
+
125
+ /**
126
+ * Capture during parsing and immediately after mounting; coordinates inside the provider are relative to its viewport.
127
+ * The article head keeps its position, width and height; the contents column keeps its position and width.
128
+ */
129
+ export function helpPanelGeometry() {
130
+ return ["[data-help-view]", "[data-cloud-app-area]", ".help-article-head", ".help-article-aside"].map((selector, index) => {
131
+ const rect = document.querySelector(selector)?.getBoundingClientRect();
132
+ if (!rect) return null;
133
+ return index < 3 ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : { x: rect.x, y: rect.y, width: rect.width };
134
+ });
135
+ }
@@ -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,4 +1,7 @@
1
1
  import { IconButton, Tooltip, useLocale } from "@k2b/ui";
2
+ import { createSignal, onCleanup, onMount } from "solid-js";
3
+ import { isServer } from "solid-js/web";
4
+ import { closePanel, openPanel } from "../browser/panel";
2
5
  import { importOnDemand } from "../browser/reload";
3
6
  import { createBusyAction } from "./busy-action";
4
7
  import { platformMessages } from "./platform-messages";
@@ -6,6 +9,8 @@ import { platformMessages } from "./platform-messages";
6
9
  type LayoutHelpTriggerProps = {
7
10
  variant: "header" | "rail";
8
11
  class?: string;
12
+ panel?: boolean;
13
+ helpOpen?: boolean;
9
14
  };
10
15
 
11
16
  /** Opens end-user help from either the desktop rail or the compact header. */
@@ -13,17 +18,35 @@ export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
13
18
  const locale = useLocale();
14
19
  const t = () => platformMessages.resolve([locale()]).t;
15
20
  const action = createBusyAction();
16
- const openHelp = () =>
17
- action.run(
21
+ // The server knows the cookie; the browser also knows the mode: a phone starts with the sheet closed, whatever the
22
+ // cookie says, and the lazy controller may have changed the state before this island mounts.
23
+ const readOpen = () => {
24
+ const canvas = document.querySelector(".cloud-app-canvas");
25
+ const attribute = window.matchMedia("(min-width: 768px)").matches ? "data-panel" : "data-panel-sheet";
26
+ return canvas?.getAttribute(attribute) === "open" && document.getElementById("cloud-panel")?.dataset.provider === "core";
27
+ };
28
+ const [helpOpen, setHelpOpen] = createSignal(isServer ? !!props.helpOpen : !!props.panel && readOpen());
29
+ onMount(() => {
30
+ const changed = (event: WindowEventMap["cloud.panel.state"]) => setHelpOpen(event.detail.open && event.detail.provider === "core");
31
+ window.addEventListener("cloud.panel.state", changed);
32
+ onCleanup(() => window.removeEventListener("cloud.panel.state", changed));
33
+ });
34
+ const openHelp = () => {
35
+ if (helpOpen()) return closePanel();
36
+ if (openPanel("core")) return;
37
+ return action.run(
18
38
  () => importOnDemand(() => import("./LayoutHelp")),
19
39
  (help) => help.openLayoutHelpDialog(),
20
40
  );
41
+ };
21
42
 
22
43
  if (props.variant === "rail")
23
44
  return (
24
45
  <Tooltip.Trigger
25
46
  type="button"
26
47
  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 ?? ""}`}
48
+ aria-controls={props.panel ? "cloud-panel" : undefined}
49
+ aria-expanded={props.panel ? helpOpen() : undefined}
27
50
  onClick={openHelp}
28
51
  aria-busy={action.busy() ? "true" : undefined}
29
52
  aria-label={t().openHelp}
@@ -38,6 +61,8 @@ export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
38
61
  return (
39
62
  <IconButton
40
63
  class={props.class}
64
+ aria-controls={props.panel ? "cloud-panel" : undefined}
65
+ aria-expanded={props.panel ? helpOpen() : undefined}
41
66
  onClick={openHelp}
42
67
  aria-busy={action.busy() ? "true" : undefined}
43
68
  label={t().openHelp}
@@ -48,8 +48,8 @@ function MobileNavigation(props: {
48
48
  value={tab}
49
49
  onValueChange={setTab}
50
50
  options={[
51
- { value: "app", label: workspace()?.label || t().appMenu },
52
- { value: "apps", label: t().allApps },
51
+ { value: "app", label: <span class="block k2b-truncate-fade">{workspace()?.label || t().appMenu}</span> },
52
+ { value: "apps", label: <span class="block k2b-truncate-fade">{t().allApps}</span> },
53
53
  ]}
54
54
  />
55
55
  }
@@ -5,12 +5,19 @@ import { getLocale } from "../server/locale";
5
5
  import { resolveRuntimeRoute } from "./app-navigation";
6
6
  import type { MinimalLayoutContext } from "./layout-context";
7
7
  import PanelRuntime from "./PanelRuntime.island";
8
+ import { readPanelContext } from "./panel-context";
8
9
  import { getRuntimeContext } from "./runtime";
9
10
 
10
- export type PanelLayoutProps = { c: MinimalLayoutContext; title: string; children: JSX.Element };
11
+ export type PanelLayoutProps = {
12
+ c: MinimalLayoutContext & { header(name: string, value: string): void };
13
+ title: string;
14
+ children: JSX.Element;
15
+ };
11
16
 
12
17
  /** A provider page embedded in the shell's same-origin panel. */
13
18
  export default function PanelLayout(props: PanelLayoutProps) {
19
+ props.c.header("Referrer-Policy", "same-origin");
20
+ const context = readPanelContext(props.c);
14
21
  const runtime = getRuntimeContext(props.c);
15
22
  // The provider owns this path by the canonical prefixes its panel declaration was checked against.
16
23
  const apps = runtime.apps.map((entry) => ({ ...entry, routes: entry.routes.map(canonicalRoute) }));
@@ -19,6 +26,7 @@ export default function PanelLayout(props: PanelLayoutProps) {
19
26
  return (
20
27
  <LocaleProvider locale={getLocale(props.c)}>
21
28
  <div class="cloud-panel-page" data-cloud-panel-page="" data-panel-provider={app?.id}>
29
+ <script id="cloud-panel-context" type="application/json" innerHTML={JSON.stringify(context).replace(/</g, "\\u003c")} />
22
30
  <PanelRuntime provider={app?.id} scope={app?.panel ? new URL(app.panel.href, props.c.req.raw.url).pathname : undefined} />
23
31
  {props.children}
24
32
  </div>
@@ -1,5 +1,6 @@
1
1
  import { installAppWorkspaceController, installInputModeController, keyboardPlatform } from "@k2b/ui";
2
2
  import { onCleanup, onMount } from "solid-js";
3
+ import { installPanelContextReceiver } from "../browser/panel-context-receiver";
3
4
  import { syncTimezoneCookie } from "./timezone-cookie";
4
5
 
5
6
  export default function PanelRuntime(props: { provider?: string; scope?: string }) {
@@ -12,6 +13,7 @@ export default function PanelRuntime(props: { provider?: string; scope?: string
12
13
  const deferred = new Set<ReturnType<typeof setTimeout>>();
13
14
  const embedded = window.parent !== window;
14
15
  let disposeTheme = () => {};
16
+ let disposeContext = () => {};
15
17
  let resumeTheme = () => {};
16
18
  // A page kept in the back/forward cache keeps following the host; only a removed iframe lets go.
17
19
  const pageHide = (event: PageTransitionEvent) => {
@@ -38,6 +40,9 @@ export default function PanelRuntime(props: { provider?: string; scope?: string
38
40
  if (event.key.toLowerCase() === "j" && event.metaKey === apple && event.ctrlKey === !apple) {
39
41
  event.preventDefault();
40
42
  if (!event.repeat) close();
43
+ } else if (event.key.toLowerCase() === "k" && event.metaKey === apple && event.ctrlKey === !apple) {
44
+ event.preventDefault();
45
+ if (!event.repeat) window.parent.postMessage({ type: "cloud.panel.search", v: 1 }, location.origin);
41
46
  } else if (event.key === "Escape" && !event.metaKey && !event.ctrlKey) {
42
47
  // An open modal dialog takes Escape through its cancel event, without preventing this keydown.
43
48
  if (document.querySelector("dialog[open], [aria-modal='true']")) return;
@@ -49,6 +54,7 @@ export default function PanelRuntime(props: { provider?: string; scope?: string
49
54
  }
50
55
  };
51
56
  if (embedded) {
57
+ disposeContext = installPanelContextReceiver();
52
58
  try {
53
59
  const parent = window.parent;
54
60
  const parentRoot = parent.document.documentElement;
@@ -73,6 +79,7 @@ export default function PanelRuntime(props: { provider?: string; scope?: string
73
79
  window.addEventListener("click", click);
74
80
  }
75
81
  onCleanup(() => {
82
+ disposeContext();
76
83
  disposeInput();
77
84
  disposeWorkspace();
78
85
  disposeTheme();
@@ -1,5 +1,6 @@
1
- import { Button, IconButton, type KeyboardPlatform, Placeholder, ResizeSeparator, useLocale } from "@k2b/ui";
1
+ import { Button, IconButton, type KeyboardPlatform, NoticeCard, Placeholder, ResizeSeparator, useLocale } from "@k2b/ui";
2
2
  import { PANEL_MAX_WIDTH_REM, PANEL_MIN_WIDTH_REM, type PanelCookieState } from "../_internal/panel-cookie";
3
+ import { withPanelContext } from "../contracts/panel-context-plain";
3
4
  import { platformMessages } from "./platform-messages";
4
5
  import type { ShellData } from "./shell-data";
5
6
 
@@ -9,6 +10,10 @@ export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]
9
10
  const t = platformMessages.resolve([locale()]).t;
10
11
  const provider = props.panel.providers.find((provider) => provider.id === props.panel.provider) ?? props.panel.providers[0];
11
12
  if (!provider) return null;
13
+ const answer = props.panel.consent?.provider === provider.id ? props.panel.consent.allowed : null;
14
+ const ask = props.state.open && provider.consent && answer === null;
15
+ const allowed = !provider.consent || answer === true;
16
+ const hasConsent = props.panel.providers.some((provider) => provider.consent);
12
17
  const tabs = props.panel.providers.length > 1;
13
18
  return (
14
19
  <aside
@@ -18,6 +23,10 @@ export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]
18
23
  aria-label={t.panel}
19
24
  data-state={props.state.open ? "loading" : "closed"}
20
25
  data-provider={provider.id}
26
+ data-app-name={provider.appName}
27
+ data-provider-consent={String(provider.consent)}
28
+ data-context-allowed={String(allowed)}
29
+ data-consent={ask ? "ask" : undefined}
21
30
  >
22
31
  <ResizeSeparator
23
32
  class="cloud-panel__resize"
@@ -48,7 +57,7 @@ export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]
48
57
  tabindex={item.id === provider.id ? 0 : -1}
49
58
  >
50
59
  <i class={item.icon} aria-hidden="true" />
51
- <span class="cloud-panel__tab-label">{item.label}</span>
60
+ <span class="cloud-panel__tab-label k2b-truncate-fade">{item.label}</span>
52
61
  </button>
53
62
  ))}
54
63
  </div>
@@ -58,6 +67,19 @@ export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]
58
67
  <span>{provider.label}</span>
59
68
  </h2>
60
69
  )}
70
+ {/* The controller mounts the ⋯ menu over this button; the place stays reserved for Core providers. */}
71
+ {hasConsent && (
72
+ <span
73
+ class="cloud-panel__menu"
74
+ data-cloud-panel-menu=""
75
+ style={{ visibility: provider.consent ? "visible" : "hidden" }}
76
+ inert={!provider.consent}
77
+ >
78
+ <IconButton label={t.panelMenu} title={t.panelMenu} aria-haspopup="menu" aria-expanded="false">
79
+ <i class="ti ti-dots" aria-hidden="true" />
80
+ </IconButton>
81
+ </span>
82
+ )}
61
83
  <IconButton
62
84
  label={t.closePanel}
63
85
  title={`${t.closePanel} (${props.platform === "apple" ? "⌘J" : "Ctrl+J"})`}
@@ -67,6 +89,34 @@ export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]
67
89
  </IconButton>
68
90
  </div>
69
91
  <div class="cloud-panel__body" id="cloud-panel-body" role={tabs ? "tabpanel" : undefined}>
92
+ {hasConsent && (
93
+ // The shell asks, not the provider: the question sits above the provider page, which loads without context.
94
+ <NoticeCard
95
+ class="cloud-panel__consent"
96
+ tone="neutral"
97
+ data-cloud-panel-consent=""
98
+ role="region"
99
+ tabindex="-1"
100
+ aria-labelledby="cloud-panel-consent-title"
101
+ title={
102
+ <span id="cloud-panel-consent-title" data-cloud-panel-consent-title="">
103
+ {t.panelConsentTitle({ app: provider.appName })}
104
+ </span>
105
+ }
106
+ detail={<span data-cloud-panel-consent-body="">{t.panelConsentBody({ app: provider.appName })}</span>}
107
+ >
108
+ <div class="cloud-panel__consent-actions">
109
+ <Button size="sm" variant="primary" data-cloud-panel-consent-answer="allow">
110
+ {t.panelConsentAllow}
111
+ </Button>
112
+ <Button size="sm" variant="secondary" data-cloud-panel-consent-answer="deny">
113
+ {t.panelConsentDeny}
114
+ </Button>
115
+ </div>
116
+ {/* A failed save reports in this reserved line, so the question never grows. */}
117
+ <p class="cloud-panel__consent-error" data-cloud-panel-consent-error="" role="status" />
118
+ </NoticeCard>
119
+ )}
70
120
  <div class="cloud-panel__skeleton" aria-hidden="true">
71
121
  <span />
72
122
  <span />
@@ -76,7 +126,7 @@ export default function PanelSlot(props: { panel: NonNullable<ShellData["panel"]
76
126
  {props.state.open && (
77
127
  <iframe
78
128
  class="cloud-panel__frame"
79
- src={provider.href}
129
+ src={withPanelContext(provider.href, allowed ? props.panel.context : null)}
80
130
  title={provider.label}
81
131
  loading="lazy"
82
132
  data-cloud-panel-frame={provider.id}